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 88f27910802..14e99611224 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 @@ -18,7 +18,6 @@ import com.google.android.exoplayer.C; import com.google.android.exoplayer.MediaFormat; import com.google.android.exoplayer.TrackRenderer; -import com.google.android.exoplayer.parser.ts.TsExtractor; import com.google.android.exoplayer.upstream.DataSource; import com.google.android.exoplayer.upstream.DataSpec; import com.google.android.exoplayer.upstream.NonBlockingInputStream; @@ -40,10 +39,10 @@ public class HlsChunkSource { private final DataSource dataSource; - private final TsExtractor extractor; private final HlsMasterPlaylist masterPlaylist; private final HlsMediaPlaylistParser mediaPlaylistParser; + private long liveStartTimeUs; /* package */ HlsMediaPlaylist mediaPlaylist; /* package */ boolean mediaPlaylistWasLive; /* package */ long lastMediaPlaylistLoadTimeMs; @@ -52,7 +51,6 @@ public class HlsChunkSource { public HlsChunkSource(DataSource dataSource, HlsMasterPlaylist masterPlaylist) { this.dataSource = dataSource; this.masterPlaylist = masterPlaylist; - extractor = new TsExtractor(); mediaPlaylistParser = new HlsMediaPlaylistParser(); } @@ -120,6 +118,7 @@ public void getChunkOperation(List queue, long seekPositionUs, long pla } } } else { + // Not live. if (queue.isEmpty()) { chunkMediaSequence = Util.binarySearchFloor(mediaPlaylist.segments, seekPositionUs, true, true) + mediaPlaylist.mediaSequence; @@ -151,14 +150,26 @@ public void getChunkOperation(List queue, long seekPositionUs, long pla long startTimeUs = segment.startTimeUs; long endTimeUs = startTimeUs + (long) (segment.durationSecs * 1000000); - int nextChunkMediaSequence = chunkMediaSequence + 1; - if (!mediaPlaylist.live && chunkIndex == mediaPlaylist.segments.size() - 1) { - nextChunkMediaSequence = -1; + + if (mediaPlaylistWasLive) { + if (queue.isEmpty()) { + liveStartTimeUs = startTimeUs; + startTimeUs = 0; + endTimeUs -= liveStartTimeUs; + } else { + startTimeUs -= liveStartTimeUs; + endTimeUs -= liveStartTimeUs; + } + } else { + // Not live. + if (chunkIndex == mediaPlaylist.segments.size() - 1) { + nextChunkMediaSequence = -1; + } } - out.chunk = new TsChunk(dataSource, dataSpec, 0, extractor, startTimeUs, endTimeUs, - nextChunkMediaSequence, segment.discontinuity); + out.chunk = new TsChunk(dataSource, dataSpec, 0, startTimeUs, endTimeUs, nextChunkMediaSequence, + segment.discontinuity); } private boolean shouldRerequestMediaPlaylist() { 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 c829d169cbf..9a289f9209a 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 @@ -23,8 +23,10 @@ import com.google.android.exoplayer.SampleSource; import com.google.android.exoplayer.TrackInfo; import com.google.android.exoplayer.TrackRenderer; +import com.google.android.exoplayer.parser.ts.TsExtractor; import com.google.android.exoplayer.upstream.Loader; import com.google.android.exoplayer.upstream.Loader.Loadable; +import com.google.android.exoplayer.upstream.NonBlockingInputStream; import com.google.android.exoplayer.util.Assertions; import android.os.SystemClock; @@ -43,8 +45,10 @@ */ public class HlsSampleSource implements SampleSource, Loader.Callback { + private static final long MAX_SAMPLE_INTERLEAVING_OFFSET_US = 5000000; private static final int NO_RESET_PENDING = -1; + private final TsExtractor extractor; private final LoadControl loadControl; private final HlsChunkSource chunkSource; private final HlsChunkOperationHolder currentLoadableHolder; @@ -73,9 +77,6 @@ public class HlsSampleSource implements SampleSource, Loader.Callback { private int currentLoadableExceptionCount; private long currentLoadableExceptionTimestamp; - private boolean pendingTimestampOffsetUpdate; - private long timestampOffsetUs; - public HlsSampleSource(HlsChunkSource chunkSource, LoadControl loadControl, int bufferSizeContribution, boolean frameAccurateSeeking, int downstreamRendererCount) { this.chunkSource = chunkSource; @@ -83,6 +84,7 @@ public HlsSampleSource(HlsChunkSource chunkSource, LoadControl loadControl, this.bufferSizeContribution = bufferSizeContribution; this.frameAccurateSeeking = frameAccurateSeeking; this.remainingReleaseCount = downstreamRendererCount; + extractor = new TsExtractor(); currentLoadableHolder = new HlsChunkOperationHolder(); mediaChunks = new LinkedList(); readOnlyHlsChunks = Collections.unmodifiableList(mediaChunks); @@ -93,32 +95,23 @@ public boolean prepare() throws IOException { if (prepared) { return true; } - if (loader == null) { loader = new Loader("Loader:HLS"); loadControl.register(this, bufferSizeContribution); } - updateLoadControl(); - if (mediaChunks.isEmpty()) { - return false; - } - TsChunk mediaChunk = mediaChunks.getFirst(); - if (mediaChunk.prepare()) { - trackCount = mediaChunk.getTrackCount(); + continueBufferingInternal(); + if (extractor.isPrepared()) { + trackCount = extractor.getTrackCount(); trackEnabledStates = new boolean[trackCount]; pendingDiscontinuities = new boolean[trackCount]; downstreamMediaFormats = new MediaFormat[trackCount]; trackInfos = new TrackInfo[trackCount]; for (int i = 0; i < trackCount; i++) { - MediaFormat format = mediaChunk.getMediaFormat(i); + MediaFormat format = extractor.getFormat(i); trackInfos[i] = new TrackInfo(format.mimeType, chunkSource.getDurationUs()); } prepared = true; } - - if (!prepared && currentLoadableException != null) { - throw currentLoadableException; - } return prepared; } @@ -142,9 +135,7 @@ public void enable(int track, long positionUs) { trackEnabledStates[track] = true; downstreamMediaFormats[track] = null; if (enabledTrackCount == 1) { - downstreamPositionUs = positionUs; - lastSeekPositionUs = positionUs; - restartFrom(positionUs); + seekToUs(positionUs); } } @@ -170,72 +161,69 @@ public boolean continueBuffering(long playbackPositionUs) throws IOException { Assertions.checkState(prepared); Assertions.checkState(enabledTrackCount > 0); downstreamPositionUs = playbackPositionUs; + return continueBufferingInternal(); + } + + private boolean continueBufferingInternal() throws IOException { updateLoadControl(); - if (isPendingReset() || mediaChunks.isEmpty()) { + if (isPendingReset()) { return false; - } else if (mediaChunks.getFirst().sampleAvailable()) { - // There's a sample available to be read from the current chunk. - return true; - } else { - // It may be the case that the current chunk has been fully read but not yet discarded and - // that the next chunk has an available sample. Return true if so, otherwise false. - return mediaChunks.size() > 1 && mediaChunks.get(1).sampleAvailable(); } - } - @Override - public int readData(int track, long playbackPositionUs, MediaFormatHolder formatHolder, - SampleHolder sampleHolder, boolean onlyReadDiscontinuity) throws IOException { - Assertions.checkState(prepared); - - if (pendingDiscontinuities[track]) { - pendingDiscontinuities[track] = false; - return DISCONTINUITY_READ; + TsChunk mediaChunk = mediaChunks.getFirst(); + if (mediaChunk.isReadFinished() && mediaChunks.size() > 1) { + discardDownstreamHlsChunk(); + mediaChunk = mediaChunks.getFirst(); } - if (onlyReadDiscontinuity) { - return NOTHING_READ; + boolean haveSufficientSamples = false; + if (mediaChunk.hasPendingDiscontinuity()) { + if (extractor.hasSamples()) { + // There are samples from before the discontinuity yet to be read from the extractor, so + // we don't want to reset the extractor yet. + haveSufficientSamples = true; + } else { + extractor.reset(mediaChunk.startTimeUs); + for (int i = 0; i < pendingDiscontinuities.length; i++) { + pendingDiscontinuities[i] = true; + } + mediaChunk.clearPendingDiscontinuity(); + } } - downstreamPositionUs = playbackPositionUs; - if (isPendingReset()) { - if (currentLoadableException != null) { - throw currentLoadableException; + if (!mediaChunk.hasPendingDiscontinuity()) { + // Allow the extractor to consume from the current chunk. + NonBlockingInputStream inputStream = mediaChunk.getNonBlockingInputStream(); + haveSufficientSamples = extractor.consumeUntil(inputStream, + downstreamPositionUs + MAX_SAMPLE_INTERLEAVING_OFFSET_US); + // If we can't read any more, then we always say we have sufficient samples. + if (!haveSufficientSamples) { + haveSufficientSamples = mediaChunk.isLastChunk() && mediaChunk.isReadFinished(); } - return NOTHING_READ; } - TsChunk mediaChunk = mediaChunks.getFirst(); + if (!haveSufficientSamples && currentLoadableException != null) { + throw currentLoadableException; + } + return haveSufficientSamples; + } - if (mediaChunk.readDiscontinuity()) { - pendingTimestampOffsetUpdate = true; - for (int i = 0; i < pendingDiscontinuities.length; i++) { - pendingDiscontinuities[i] = true; - } + @Override + public int readData(int track, long playbackPositionUs, MediaFormatHolder formatHolder, + SampleHolder sampleHolder, boolean onlyReadDiscontinuity) { + Assertions.checkState(prepared); + downstreamPositionUs = playbackPositionUs; + + if (pendingDiscontinuities[track]) { pendingDiscontinuities[track] = false; return DISCONTINUITY_READ; } - if (mediaChunk.isReadFinished()) { - // We've read all of the samples from the current media chunk. - if (mediaChunks.size() > 1) { - discardDownstreamHlsChunk(); - mediaChunk = mediaChunks.getFirst(); - return readData(track, playbackPositionUs, formatHolder, sampleHolder, false); - } else if (mediaChunk.isLastChunk()) { - return END_OF_STREAM; - } + if (onlyReadDiscontinuity || isPendingReset() || !extractor.isPrepared()) { return NOTHING_READ; } - if (!mediaChunk.prepare()) { - if (currentLoadableException != null) { - throw currentLoadableException; - } - return NOTHING_READ; - } - - MediaFormat mediaFormat = mediaChunk.getMediaFormat(track); + MediaFormat mediaFormat = extractor.getFormat(track); if (mediaFormat != null && !mediaFormat.equals(downstreamMediaFormats[track], true)) { chunkSource.getMaxVideoDimensions(mediaFormat); formatHolder.format = mediaFormat; @@ -243,20 +231,17 @@ public int readData(int track, long playbackPositionUs, MediaFormatHolder format return FORMAT_READ; } - if (mediaChunk.read(track, sampleHolder)) { - if (pendingTimestampOffsetUpdate) { - pendingTimestampOffsetUpdate = false; - timestampOffsetUs = sampleHolder.timeUs - mediaChunk.startTimeUs; - } - sampleHolder.timeUs -= timestampOffsetUs; + if (extractor.getSample(track, sampleHolder)) { sampleHolder.decodeOnly = frameAccurateSeeking && sampleHolder.timeUs < lastSeekPositionUs; return SAMPLE_READ; - } else { - if (currentLoadableException != null) { - throw currentLoadableException; - } - return NOTHING_READ; } + + TsChunk mediaChunk = mediaChunks.getFirst(); + if (mediaChunk.isLastChunk() && mediaChunk.isReadFinished()) { + return END_OF_STREAM; + } + + return NOTHING_READ; } @Override @@ -276,9 +261,9 @@ public void seekToUs(long positionUs) { if (mediaChunk == null) { restartFrom(positionUs); } else { - pendingTimestampOffsetUpdate = true; - mediaChunk.reset(); discardDownstreamHlsChunks(mediaChunk); + mediaChunk.reset(); + extractor.reset(mediaChunk.startTimeUs); updateLoadControl(); } } @@ -503,12 +488,11 @@ private void maybeStartLoading() { currentLoadable.init(loadControl.getAllocator()); if (isTsChunk(currentLoadable)) { TsChunk mediaChunk = (TsChunk) currentLoadable; + mediaChunks.add(mediaChunk); if (isPendingReset()) { - pendingTimestampOffsetUpdate = true; - mediaChunk.reset(); + extractor.reset(mediaChunk.startTimeUs); pendingResetPositionUs = NO_RESET_PENDING; } - mediaChunks.add(mediaChunk); } loader.startLoading(currentLoadable, this); } 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 9da8eb7459f..ee872bef836 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,12 +15,8 @@ */ package com.google.android.exoplayer.hls; -import com.google.android.exoplayer.MediaFormat; -import com.google.android.exoplayer.SampleHolder; -import com.google.android.exoplayer.parser.ts.TsExtractor; import com.google.android.exoplayer.upstream.DataSource; import com.google.android.exoplayer.upstream.DataSpec; -import com.google.android.exoplayer.upstream.NonBlockingInputStream; /** * A MPEG2TS chunk. @@ -44,82 +40,42 @@ public final class TsChunk extends HlsChunk { */ private final boolean discontinuity; - private final TsExtractor extractor; - private boolean pendingDiscontinuity; /** * @param dataSource A {@link DataSource} for loading the data. * @param dataSpec Defines the data to be loaded. - * @param extractor The extractor that will be used to extract the samples. * @param trigger The reason for this chunk being selected. * @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 nextChunkIndex The index of the next chunk, or -1 if this is the last chunk. * @param discontinuity The encoding discontinuity indicator. */ - public TsChunk(DataSource dataSource, DataSpec dataSpec, int trigger, TsExtractor extractor, - long startTimeUs, long endTimeUs, int nextChunkIndex, boolean discontinuity) { + public TsChunk(DataSource dataSource, DataSpec dataSpec, int trigger, long startTimeUs, + long endTimeUs, int nextChunkIndex, boolean discontinuity) { super(dataSource, dataSpec, trigger); this.startTimeUs = startTimeUs; this.endTimeUs = endTimeUs; this.nextChunkIndex = nextChunkIndex; - this.extractor = extractor; this.discontinuity = discontinuity; this.pendingDiscontinuity = discontinuity; } - public boolean readDiscontinuity() { - if (pendingDiscontinuity) { - extractor.reset(); - pendingDiscontinuity = false; - return true; - } - return false; - } - - public boolean prepare() { - return extractor.prepare(getNonBlockingInputStream()); - } - - public int getTrackCount() { - return extractor.getTrackCount(); - } - - public boolean sampleAvailable() { - // TODO: Maybe optimize this to not require looping over the tracks. - if (!prepare()) { - return false; - } - // TODO: Optimize this to not require looping over the tracks. - NonBlockingInputStream inputStream = getNonBlockingInputStream(); - int trackCount = extractor.getTrackCount(); - for (int i = 0; i < trackCount; i++) { - int result = extractor.read(inputStream, i, null); - if ((result & TsExtractor.RESULT_NEED_SAMPLE_HOLDER) != 0) { - return true; - } - } - return false; - } - - public boolean read(int track, SampleHolder holder) { - int result = extractor.read(getNonBlockingInputStream(), track, holder); - return (result & TsExtractor.RESULT_READ_SAMPLE) != 0; + public boolean isLastChunk() { + return nextChunkIndex == -1; } public void reset() { - extractor.reset(); - pendingDiscontinuity = discontinuity; resetReadPosition(); + pendingDiscontinuity = discontinuity; } - public MediaFormat getMediaFormat(int track) { - return extractor.getFormat(track); + public boolean hasPendingDiscontinuity() { + return pendingDiscontinuity; } - public boolean isLastChunk() { - return nextChunkIndex == -1; + public void clearPendingDiscontinuity() { + pendingDiscontinuity = false; } } diff --git a/library/src/main/java/com/google/android/exoplayer/parser/ts/TsExtractor.java b/library/src/main/java/com/google/android/exoplayer/parser/ts/TsExtractor.java index 8b4099ef77a..29fe07c1bf4 100644 --- a/library/src/main/java/com/google/android/exoplayer/parser/ts/TsExtractor.java +++ b/library/src/main/java/com/google/android/exoplayer/parser/ts/TsExtractor.java @@ -37,19 +37,6 @@ */ public final class TsExtractor { - /** - * An attempt to read from the input stream returned insufficient data. - */ - public static final int RESULT_NEED_MORE_DATA = 1; - /** - * A media sample was read. - */ - public static final int RESULT_READ_SAMPLE = 2; - /** - * The next thing to be read is a sample, but a {@link SampleHolder} was not supplied. - */ - public static final int RESULT_NEED_SAMPLE_HOLDER = 4; - private static final String TAG = "TsExtractor"; private static final int TS_PACKET_SIZE = 188; @@ -69,12 +56,18 @@ public final class TsExtractor { private boolean prepared; + private boolean pendingTimestampOffsetUpdate; + private long pendingTimestampOffsetUs; + private long sampleTimestampOffsetUs; + private long largestParsedTimestampUs; + public TsExtractor() { tsPacketBuffer = new BitsArray(); pesPayloadReaders = new SparseArray(); tsPayloadReaders = new SparseArray(); tsPayloadReaders.put(TS_PAT_PID, new PatReader()); samplesPool = new LinkedList(); + largestParsedTimestampUs = Long.MIN_VALUE; } /** @@ -102,10 +95,19 @@ public MediaFormat getFormat(int track) { return pesPayloadReaders.valueAt(track).getMediaFormat(); } + /** + * Whether the extractor is prepared. + * + * @return True if the extractor is prepared. False otherwise. + */ + public boolean isPrepared() { + return prepared; + } + /** * Resets the extractor's internal state. */ - public void reset() { + public void reset(long nextSampleTimestampUs) { prepared = false; tsPacketBuffer.reset(); tsPayloadReaders.clear(); @@ -115,72 +117,78 @@ public void reset() { pesPayloadReaders.valueAt(i).clear(); } pesPayloadReaders.clear(); + // Configure for subsequent read operations. + pendingTimestampOffsetUpdate = true; + pendingTimestampOffsetUs = nextSampleTimestampUs; + largestParsedTimestampUs = Long.MIN_VALUE; } /** - * Attempts to prepare the extractor. The extractor is prepared once it has read sufficient data - * to have established the available tracks and their corresponding media formats. + * Consumes data from a {@link NonBlockingInputStream}. *

- * Calling this method is a no-op if the extractor is already prepared. + * The read terminates if the end of the input stream is reached, if insufficient data is + * available to read a sample, or if the extractor has consumed up to the specified target + * timestamp. * - * @param inputStream The input stream from which data can be read. - * @return True if the extractor was prepared. False if more data is required. + * @param inputStream The input stream from which data should be read. + * @param targetTimestampUs A target timestamp to consume up to. + * @return True if the target timestamp was reached. False otherwise. */ - public boolean prepare(NonBlockingInputStream inputStream) { - while (!prepared) { - if (readTSPacket(inputStream) == -1) { - return false; - } + public boolean consumeUntil(NonBlockingInputStream inputStream, long targetTimestampUs) { + while (largestParsedTimestampUs < targetTimestampUs && readTSPacket(inputStream) != -1) { + // Carry on. + } + if (!prepared) { prepared = checkPrepared(); } - return true; + return largestParsedTimestampUs >= targetTimestampUs; } - private boolean checkPrepared() { - int pesPayloadReaderCount = pesPayloadReaders.size(); - if (pesPayloadReaderCount == 0) { + /** + * Gets the next sample for the specified track. + * + * @param track The track from which to read. + * @param out A {@link SampleHolder} into which the next sample should be read. + * @return True if a sample was read. False otherwise. + */ + public boolean getSample(int track, SampleHolder out) { + Assertions.checkState(prepared); + Queue queue = pesPayloadReaders.valueAt(track).samplesQueue; + if (queue.isEmpty()) { return false; } - for (int i = 0; i < pesPayloadReaderCount; i++) { - if (!pesPayloadReaders.valueAt(i).hasMediaFormat()) { - return false; - } - } + Sample sample = queue.remove(); + convert(sample, out); + recycleSample(sample); return true; } /** - * Consumes data from a {@link NonBlockingInputStream}. - *

- * The read terminates if the end of the input stream is reached, if insufficient data is - * available to read a sample, or if a sample is read. The returned flags indicate - * both the reason for termination and data that was parsed during the read. + * Whether samples are available for reading from {@link #getSample(int, SampleHolder)}. * - * @param inputStream The input stream from which data should be read. - * @param track The track from which to read. - * @param out A {@link SampleHolder} into which the next sample should be read. If null then - * {@link #RESULT_NEED_SAMPLE_HOLDER} will be returned once a sample has been reached. - * @return One or more of the {@code RESULT_*} flags defined in this class. + * @return True if samples are available for reading from {@link #getSample(int, SampleHolder)}. + * False otherwise. */ - public int read(NonBlockingInputStream inputStream, int track, SampleHolder out) { - Assertions.checkState(prepared); - Queue queue = pesPayloadReaders.valueAt(track).samplesQueue; - - // Keep reading if the buffer is empty. - while (queue.isEmpty()) { - if (readTSPacket(inputStream) == -1) { - return RESULT_NEED_MORE_DATA; + public boolean hasSamples() { + for (int i = 0; i < pesPayloadReaders.size(); i++) { + if (!pesPayloadReaders.valueAt(i).samplesQueue.isEmpty()) { + return true; } } + return false; + } - if (!queue.isEmpty() && out == null) { - return RESULT_NEED_SAMPLE_HOLDER; + private boolean checkPrepared() { + int pesPayloadReaderCount = pesPayloadReaders.size(); + if (pesPayloadReaderCount == 0) { + return false; } - - Sample sample = queue.remove(); - convert(sample, out); - recycleSample(sample); - return RESULT_READ_SAMPLE; + for (int i = 0; i < pesPayloadReaderCount; i++) { + if (!pesPayloadReaders.valueAt(i).hasMediaFormat()) { + return false; + } + } + return true; } /** @@ -506,6 +514,12 @@ protected void addSample(BitsArray buffer, int sampleSize, long sampleTimeUs, in addToSample(sample, buffer, sampleSize); sample.flags = flags; sample.timeUs = sampleTimeUs; + addSample(sample); + } + + protected void addSample(Sample sample) { + adjustTimestamp(sample); + largestParsedTimestampUs = Math.max(largestParsedTimestampUs, sample.timeUs); samplesQueue.add(sample); } @@ -517,6 +531,14 @@ protected void addToSample(Sample sample, BitsArray buffer, int size) { sample.size += size; } + private void adjustTimestamp(Sample sample) { + if (pendingTimestampOffsetUpdate) { + sampleTimestampOffsetUs = pendingTimestampOffsetUs - sample.timeUs; + pendingTimestampOffsetUpdate = false; + } + sample.timeUs += sampleTimestampOffsetUs; + } + } /** @@ -549,7 +571,7 @@ public void read(BitsArray pesBuffer, int pesPayloadSize, long pesTimeUs) { // Single PES packet should contain only one new H.264 frame. if (currentSample != null) { - samplesQueue.add(currentSample); + addSample(currentSample); } currentSample = getSample(); pesPayloadSize -= readOneH264Frame(pesBuffer, false);