/* * 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; import com.google.android.exoplayer.C; import com.google.android.exoplayer.LoadControl; import com.google.android.exoplayer.MediaFormat; import com.google.android.exoplayer.MediaFormatHolder; import com.google.android.exoplayer.SampleHolder; import com.google.android.exoplayer.SampleSource; import com.google.android.exoplayer.SampleSource.SampleSourceReader; import com.google.android.exoplayer.TrackRenderer; import com.google.android.exoplayer.chunk.BaseChunkSampleSourceEventListener; import com.google.android.exoplayer.chunk.Chunk; import com.google.android.exoplayer.chunk.ChunkOperationHolder; import com.google.android.exoplayer.chunk.Format; import com.google.android.exoplayer.upstream.Loader; import com.google.android.exoplayer.upstream.Loader.Loadable; import com.google.android.exoplayer.util.Assertions; import com.google.android.exoplayer.util.MimeTypes; import android.os.Handler; import android.os.SystemClock; import java.io.IOException; import java.util.LinkedList; /** * A {@link SampleSource} for HLS streams. */ public final class HlsSampleSource implements SampleSource, SampleSourceReader, Loader.Callback { /** * Interface definition for a callback to be notified of {@link HlsSampleSource} events. */ public interface EventListener extends BaseChunkSampleSourceEventListener {} /** * The default minimum number of times to retry loading data prior to failing. */ public static final int DEFAULT_MIN_LOADABLE_RETRY_COUNT = 3; private static final long NO_RESET_PENDING = Long.MIN_VALUE; private final HlsChunkSource chunkSource; private final LinkedList<HlsExtractorWrapper> extractors; private final int minLoadableRetryCount; private final int bufferSizeContribution; private final ChunkOperationHolder chunkOperationHolder; private final int eventSourceId; private final LoadControl loadControl; private final Handler eventHandler; private final EventListener eventListener; private int remainingReleaseCount; private boolean prepared; private boolean loadControlRegistered; private int trackCount; private int enabledTrackCount; private boolean[] trackEnabledStates; private boolean[] pendingDiscontinuities; private MediaFormat[] trackFormat; private MediaFormat[] downstreamMediaFormats; private Format downstreamFormat; private long downstreamPositionUs; private long lastSeekPositionUs; private long pendingResetPositionUs; private boolean loadingFinished; private Chunk currentLoadable; private TsChunk currentTsLoadable; private TsChunk previousTsLoadable; private Loader loader; private IOException currentLoadableException; private int currentLoadableExceptionCount; private long currentLoadableExceptionTimestamp; private long currentLoadStartTimeMs; public HlsSampleSource(HlsChunkSource chunkSource, LoadControl loadControl, int bufferSizeContribution) { this(chunkSource, loadControl, bufferSizeContribution, null, null, 0); } public HlsSampleSource(HlsChunkSource chunkSource, LoadControl loadControl, int bufferSizeContribution, Handler eventHandler, EventListener eventListener, int eventSourceId) { this(chunkSource, loadControl, bufferSizeContribution, eventHandler, eventListener, eventSourceId, DEFAULT_MIN_LOADABLE_RETRY_COUNT); } public HlsSampleSource(HlsChunkSource chunkSource, LoadControl loadControl, int bufferSizeContribution, Handler eventHandler, EventListener eventListener, int eventSourceId, int minLoadableRetryCount) { this.chunkSource = chunkSource; this.loadControl = loadControl; this.bufferSizeContribution = bufferSizeContribution; this.minLoadableRetryCount = minLoadableRetryCount; this.eventHandler = eventHandler; this.eventListener = eventListener; this.eventSourceId = eventSourceId; this.pendingResetPositionUs = NO_RESET_PENDING; extractors = new LinkedList<>(); chunkOperationHolder = new ChunkOperationHolder(); } @Override public SampleSourceReader register() { remainingReleaseCount++; return this; } @Override public boolean prepare(long positionUs) { if (prepared) { return true; } if (!extractors.isEmpty()) { // We're not prepared, but we might have loaded what we need. HlsExtractorWrapper extractor = getCurrentExtractor(); if (extractor.isPrepared()) { trackCount = extractor.getTrackCount(); trackEnabledStates = new boolean[trackCount]; pendingDiscontinuities = new boolean[trackCount]; downstreamMediaFormats = new MediaFormat[trackCount]; trackFormat = new MediaFormat[trackCount]; long durationUs = chunkSource.getDurationUs(); for (int i = 0; i < trackCount; i++) { MediaFormat format = extractor.getMediaFormat(i).copyWithDurationUs(durationUs); if (MimeTypes.isVideo(format.mimeType)) { format = format.copyAsAdaptive(); } trackFormat[i] = format; } prepared = true; return true; } } // We're not prepared and we haven't loaded what we need. if (loader == null) { loader = new Loader("Loader:HLS"); } if (!loadControlRegistered) { loadControl.register(this, bufferSizeContribution); loadControlRegistered = true; } if (!loader.isLoading()) { // We're going to have to start loading a chunk to get what we need for preparation. We should // attempt to load the chunk at positionUs, so that we'll already be loading the correct chunk // in the common case where the renderer is subsequently enabled at this position. pendingResetPositionUs = positionUs; downstreamPositionUs = positionUs; } maybeStartLoading(); return false; } @Override public int getTrackCount() { Assertions.checkState(prepared); return trackCount; } @Override public MediaFormat getFormat(int track) { Assertions.checkState(prepared); return trackFormat[track]; } @Override public void enable(int track, long positionUs) { Assertions.checkState(prepared); Assertions.checkState(!trackEnabledStates[track]); enabledTrackCount++; trackEnabledStates[track] = true; downstreamMediaFormats[track] = null; pendingDiscontinuities[track] = false; downstreamFormat = null; if (!loadControlRegistered) { loadControl.register(this, bufferSizeContribution); loadControlRegistered = true; } if (enabledTrackCount == 1) { downstreamPositionUs = positionUs; lastSeekPositionUs = positionUs; restartFrom(positionUs); } } @Override public void disable(int track) { Assertions.checkState(prepared); Assertions.checkState(trackEnabledStates[track]); enabledTrackCount--; trackEnabledStates[track] = false; if (enabledTrackCount == 0) { chunkSource.reset(); downstreamPositionUs = Long.MIN_VALUE; if (loadControlRegistered) { loadControl.unregister(this); loadControlRegistered = false; } if (loader.isLoading()) { loader.cancelLoading(); } else { clearState(); loadControl.trimAllocator(); } } } @Override public boolean continueBuffering(int track, long playbackPositionUs) { Assertions.checkState(prepared); Assertions.checkState(trackEnabledStates[track]); downstreamPositionUs = playbackPositionUs; if (!extractors.isEmpty()) { discardSamplesForDisabledTracks(getCurrentExtractor(), downstreamPositionUs); } if (loadingFinished) { return true; } maybeStartLoading(); if (isPendingReset() || extractors.isEmpty()) { return false; } for (int extractorIndex = 0; extractorIndex < extractors.size(); extractorIndex++) { HlsExtractorWrapper extractor = extractors.get(extractorIndex); if (!extractor.isPrepared()) { break; } if (extractor.hasSamples(track)) { return true; } } return false; } @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 (onlyReadDiscontinuity) { return NOTHING_READ; } if (isPendingReset()) { return NOTHING_READ; } HlsExtractorWrapper extractor = getCurrentExtractor(); if (!extractor.isPrepared()) { return NOTHING_READ; } if (downstreamFormat == null || !downstreamFormat.equals(extractor.format)) { // Notify a change in the downstream format. notifyDownstreamFormatChanged(extractor.format, extractor.trigger, extractor.startTimeUs); downstreamFormat = extractor.format; } 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. extractor.configureSpliceTo(extractors.get(1)); } int extractorIndex = 0; while (extractors.size() > extractorIndex + 1 && !extractor.hasSamples(track)) { // We're finished reading from the extractor for this particular track, so advance to the // next one for the current read. extractor = extractors.get(++extractorIndex); if (!extractor.isPrepared()) { return NOTHING_READ; } } MediaFormat mediaFormat = extractor.getMediaFormat(track); if (mediaFormat != null && !mediaFormat.equals(downstreamMediaFormats[track])) { formatHolder.format = mediaFormat; downstreamMediaFormats[track] = mediaFormat; return FORMAT_READ; } if (extractor.getSample(track, sampleHolder)) { boolean decodeOnly = sampleHolder.timeUs < lastSeekPositionUs; sampleHolder.flags |= decodeOnly ? C.SAMPLE_FLAG_DECODE_ONLY : 0; return SAMPLE_READ; } if (loadingFinished) { return END_OF_STREAM; } return NOTHING_READ; } @Override public void maybeThrowError() throws IOException { if (currentLoadableException != null && currentLoadableExceptionCount > minLoadableRetryCount) { throw currentLoadableException; } else if (currentLoadable == null) { chunkSource.maybeThrowError(); } } @Override public void seekToUs(long positionUs) { Assertions.checkState(prepared); Assertions.checkState(enabledTrackCount > 0); long currentPositionUs = isPendingReset() ? pendingResetPositionUs : downstreamPositionUs; downstreamPositionUs = positionUs; lastSeekPositionUs = positionUs; if (currentPositionUs == positionUs) { return; } // TODO: Optimize the seek for the case where the position is already buffered. downstreamPositionUs = positionUs; for (int i = 0; i < pendingDiscontinuities.length; i++) { pendingDiscontinuities[i] = true; } restartFrom(positionUs); } @Override public long getBufferedPositionUs() { Assertions.checkState(prepared); Assertions.checkState(enabledTrackCount > 0); if (isPendingReset()) { return pendingResetPositionUs; } else if (loadingFinished) { return TrackRenderer.END_OF_TRACK_US; } else { long largestParsedTimestampUs = extractors.getLast().getLargestParsedTimestampUs(); if (extractors.size() > 1) { // When adapting from one format to the next, the penultimate extractor may have the largest // parsed timestamp (e.g. if the last extractor hasn't parsed any timestamps yet). largestParsedTimestampUs = Math.max(largestParsedTimestampUs, extractors.get(extractors.size() - 2).getLargestParsedTimestampUs()); } return largestParsedTimestampUs == Long.MIN_VALUE ? downstreamPositionUs : largestParsedTimestampUs; } } @Override public void release() { Assertions.checkState(remainingReleaseCount > 0); if (--remainingReleaseCount == 0 && loader != null) { loader.release(); loader = null; } } // Loader.Callback implementation. @Override public void onLoadCompleted(Loadable loadable) { Assertions.checkState(loadable == currentLoadable); long now = SystemClock.elapsedRealtime(); long loadDurationMs = now - currentLoadStartTimeMs; chunkSource.onChunkLoadCompleted(currentLoadable); if (isTsChunk(currentLoadable)) { Assertions.checkState(currentLoadable == currentTsLoadable); previousTsLoadable = currentTsLoadable; notifyLoadCompleted(currentLoadable.bytesLoaded(), currentTsLoadable.type, currentTsLoadable.trigger, currentTsLoadable.format, currentTsLoadable.startTimeUs, currentTsLoadable.endTimeUs, now, loadDurationMs); } else { notifyLoadCompleted(currentLoadable.bytesLoaded(), currentLoadable.type, currentLoadable.trigger, currentLoadable.format, -1, -1, now, loadDurationMs); } clearCurrentLoadable(); if (enabledTrackCount > 0 || !prepared) { maybeStartLoading(); } } @Override public void onLoadCanceled(Loadable loadable) { notifyLoadCanceled(currentLoadable.bytesLoaded()); if (enabledTrackCount > 0) { restartFrom(pendingResetPositionUs); } else { clearState(); loadControl.trimAllocator(); } } @Override public void onLoadError(Loadable loadable, IOException e) { if (chunkSource.onChunkLoadError(currentLoadable, e)) { // Error handled by source. if (previousTsLoadable == null && !isPendingReset()) { pendingResetPositionUs = lastSeekPositionUs; } clearCurrentLoadable(); } else { currentLoadableException = e; currentLoadableExceptionCount++; currentLoadableExceptionTimestamp = SystemClock.elapsedRealtime(); } notifyLoadError(e); maybeStartLoading(); } // Internal stuff. /** * Gets the current extractor from which samples should be read. * <p> * Calling this method discards extractors without any samples from the front of the queue. The * last extractor is retained even if it doesn't have any samples. * <p> * This method must not be called unless {@link #extractors} is non-empty. * * @return The current extractor from which samples should be read. Guaranteed to be non-null. */ 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().clear(); extractor = extractors.getFirst(); } return extractor; } private void discardSamplesForDisabledTracks(HlsExtractorWrapper extractor, long timeUs) { if (!extractor.isPrepared()) { return; } for (int i = 0; i < trackEnabledStates.length; i++) { if (!trackEnabledStates[i]) { extractor.discardUntil(i, timeUs); } } } private boolean haveSamplesForEnabledTracks(HlsExtractorWrapper extractor) { if (!extractor.isPrepared()) { return false; } for (int i = 0; i < trackEnabledStates.length; i++) { if (trackEnabledStates[i] && extractor.hasSamples(i)) { return true; } } return false; } private void restartFrom(long positionUs) { pendingResetPositionUs = positionUs; loadingFinished = false; if (loader.isLoading()) { loader.cancelLoading(); } else { clearState(); maybeStartLoading(); } } private void clearState() { for (int i = 0; i < extractors.size(); i++) { extractors.get(i).clear(); } extractors.clear(); clearCurrentLoadable(); previousTsLoadable = null; } private void clearCurrentLoadable() { currentTsLoadable = null; currentLoadable = null; currentLoadableException = null; currentLoadableExceptionCount = 0; } private void maybeStartLoading() { long now = SystemClock.elapsedRealtime(); long nextLoadPositionUs = getNextLoadPositionUs(); boolean isBackedOff = currentLoadableException != null; boolean loadingOrBackedOff = loader.isLoading() || isBackedOff; // Update the control with our current state, and determine whether we're the next loader. boolean nextLoader = loadControl.update(this, downstreamPositionUs, nextLoadPositionUs, loadingOrBackedOff); if (isBackedOff) { long elapsedMillis = now - currentLoadableExceptionTimestamp; if (elapsedMillis >= getRetryDelayMillis(currentLoadableExceptionCount)) { currentLoadableException = null; loader.startLoading(currentLoadable, this); } return; } if (loader.isLoading() || !nextLoader) { return; } chunkSource.getChunkOperation(previousTsLoadable, pendingResetPositionUs, downstreamPositionUs, chunkOperationHolder); boolean endOfStream = chunkOperationHolder.endOfStream; Chunk nextLoadable = chunkOperationHolder.chunk; chunkOperationHolder.clear(); if (endOfStream) { loadingFinished = true; return; } if (nextLoadable == null) { return; } currentLoadStartTimeMs = now; currentLoadable = nextLoadable; if (isTsChunk(currentLoadable)) { TsChunk tsChunk = (TsChunk) currentLoadable; if (isPendingReset()) { pendingResetPositionUs = NO_RESET_PENDING; } HlsExtractorWrapper extractorWrapper = tsChunk.extractorWrapper; if (extractors.isEmpty() || extractors.getLast() != extractorWrapper) { extractorWrapper.init(loadControl.getAllocator()); extractors.addLast(extractorWrapper); } notifyLoadStarted(tsChunk.dataSpec.length, tsChunk.type, tsChunk.trigger, tsChunk.format, tsChunk.startTimeUs, tsChunk.endTimeUs); currentTsLoadable = tsChunk; } else { notifyLoadStarted(currentLoadable.dataSpec.length, currentLoadable.type, currentLoadable.trigger, currentLoadable.format, -1, -1); } loader.startLoading(currentLoadable, this); } /** * Gets the next load time, assuming that the next load starts where the previous chunk ended (or * from the pending reset time, if there is one). */ private long getNextLoadPositionUs() { if (isPendingReset()) { return pendingResetPositionUs; } else { return loadingFinished ? -1 : currentTsLoadable != null ? currentTsLoadable.endTimeUs : previousTsLoadable.endTimeUs; } } private boolean isTsChunk(Chunk chunk) { return chunk instanceof TsChunk; } private boolean isPendingReset() { return pendingResetPositionUs != NO_RESET_PENDING; } private long getRetryDelayMillis(long errorCount) { return Math.min((errorCount - 1) * 1000, 5000); } /* package */ int usToMs(long timeUs) { return (int) (timeUs / 1000); } private void notifyLoadStarted(final long length, final int type, final int trigger, final Format format, final long mediaStartTimeUs, final long mediaEndTimeUs) { if (eventHandler != null && eventListener != null) { eventHandler.post(new Runnable() { @Override public void run() { eventListener.onLoadStarted(eventSourceId, length, type, trigger, format, usToMs(mediaStartTimeUs), usToMs(mediaEndTimeUs)); } }); } } private void notifyLoadCompleted(final long bytesLoaded, final int type, final int trigger, final Format format, final long mediaStartTimeUs, final long mediaEndTimeUs, final long elapsedRealtimeMs, final long loadDurationMs) { if (eventHandler != null && eventListener != null) { eventHandler.post(new Runnable() { @Override public void run() { eventListener.onLoadCompleted(eventSourceId, bytesLoaded, type, trigger, format, usToMs(mediaStartTimeUs), usToMs(mediaEndTimeUs), elapsedRealtimeMs, loadDurationMs); } }); } } private void notifyLoadCanceled(final long bytesLoaded) { if (eventHandler != null && eventListener != null) { eventHandler.post(new Runnable() { @Override public void run() { eventListener.onLoadCanceled(eventSourceId, bytesLoaded); } }); } } private void notifyLoadError(final IOException e) { if (eventHandler != null && eventListener != null) { eventHandler.post(new Runnable() { @Override public void run() { eventListener.onLoadError(eventSourceId, e); } }); } } private void notifyDownstreamFormatChanged(final Format format, final int trigger, final long positionUs) { if (eventHandler != null && eventListener != null) { eventHandler.post(new Runnable() { @Override public void run() { eventListener.onDownstreamFormatChanged(eventSourceId, format, trigger, usToMs(positionUs)); } }); } } }