/* * 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.chunk; 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.extractor.DefaultTrackOutput; import com.google.android.exoplayer.upstream.Loader; import com.google.android.exoplayer.upstream.Loader.Loadable; import com.google.android.exoplayer.util.Assertions; import android.os.Handler; import android.os.SystemClock; import java.io.IOException; import java.util.Collections; import java.util.LinkedList; import java.util.List; /** * A {@link SampleSource} that loads media in {@link Chunk}s, which are themselves obtained from a * {@link ChunkSource}. */ public class ChunkSampleSource implements SampleSource, SampleSourceReader, Loader.Callback { /** * Interface definition for a callback to be notified of {@link ChunkSampleSource} 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 int STATE_IDLE = 0; private static final int STATE_INITIALIZED = 1; private static final int STATE_PREPARED = 2; private static final int STATE_ENABLED = 3; private static final long NO_RESET_PENDING = Long.MIN_VALUE; private final int eventSourceId; private final LoadControl loadControl; private final ChunkSource chunkSource; private final ChunkOperationHolder currentLoadableHolder; private final LinkedList<BaseMediaChunk> mediaChunks; private final List<BaseMediaChunk> readOnlyMediaChunks; private final DefaultTrackOutput sampleQueue; private final int bufferSizeContribution; private final Handler eventHandler; private final EventListener eventListener; private final int minLoadableRetryCount; private int state; private long downstreamPositionUs; private long lastSeekPositionUs; private long pendingResetPositionUs; private long lastPerformedBufferOperation; private boolean pendingDiscontinuity; private Loader loader; private boolean loadingFinished; private IOException currentLoadableException; private int enabledTrackCount; private int currentLoadableExceptionCount; private long currentLoadableExceptionTimestamp; private long currentLoadStartTimeMs; private MediaFormat downstreamMediaFormat; private Format downstreamFormat; public ChunkSampleSource(ChunkSource chunkSource, LoadControl loadControl, int bufferSizeContribution) { this(chunkSource, loadControl, bufferSizeContribution, null, null, 0); } public ChunkSampleSource(ChunkSource chunkSource, LoadControl loadControl, int bufferSizeContribution, Handler eventHandler, EventListener eventListener, int eventSourceId) { this(chunkSource, loadControl, bufferSizeContribution, eventHandler, eventListener, eventSourceId, DEFAULT_MIN_LOADABLE_RETRY_COUNT); } public ChunkSampleSource(ChunkSource chunkSource, LoadControl loadControl, int bufferSizeContribution, Handler eventHandler, EventListener eventListener, int eventSourceId, int minLoadableRetryCount) { this.chunkSource = chunkSource; this.loadControl = loadControl; this.bufferSizeContribution = bufferSizeContribution; this.eventHandler = eventHandler; this.eventListener = eventListener; this.eventSourceId = eventSourceId; this.minLoadableRetryCount = minLoadableRetryCount; currentLoadableHolder = new ChunkOperationHolder(); mediaChunks = new LinkedList<>(); readOnlyMediaChunks = Collections.unmodifiableList(mediaChunks); sampleQueue = new DefaultTrackOutput(loadControl.getAllocator()); state = STATE_IDLE; pendingResetPositionUs = NO_RESET_PENDING; } @Override public SampleSourceReader register() { Assertions.checkState(state == STATE_IDLE); state = STATE_INITIALIZED; return this; } @Override public boolean prepare(long positionUs) { Assertions.checkState(state == STATE_INITIALIZED || state == STATE_PREPARED); if (state == STATE_PREPARED) { return true; } else if (!chunkSource.prepare()) { return false; } if (chunkSource.getTrackCount() > 0) { loader = new Loader("Loader:" + chunkSource.getFormat(0).mimeType); } state = STATE_PREPARED; return true; } @Override public int getTrackCount() { Assertions.checkState(state == STATE_PREPARED || state == STATE_ENABLED); return chunkSource.getTrackCount(); } @Override public MediaFormat getFormat(int track) { Assertions.checkState(state == STATE_PREPARED || state == STATE_ENABLED); return chunkSource.getFormat(track); } @Override public void enable(int track, long positionUs) { Assertions.checkState(state == STATE_PREPARED); Assertions.checkState(enabledTrackCount++ == 0); state = STATE_ENABLED; chunkSource.enable(track); loadControl.register(this, bufferSizeContribution); downstreamFormat = null; downstreamMediaFormat = null; downstreamPositionUs = positionUs; lastSeekPositionUs = positionUs; pendingDiscontinuity = false; restartFrom(positionUs); } @Override public void disable(int track) { Assertions.checkState(state == STATE_ENABLED); Assertions.checkState(--enabledTrackCount == 0); state = STATE_PREPARED; try { chunkSource.disable(mediaChunks); } finally { loadControl.unregister(this); if (loader.isLoading()) { loader.cancelLoading(); } else { sampleQueue.clear(); mediaChunks.clear(); clearCurrentLoadable(); loadControl.trimAllocator(); } } } @Override public boolean continueBuffering(int track, long positionUs) { Assertions.checkState(state == STATE_ENABLED); downstreamPositionUs = positionUs; chunkSource.continueBuffering(positionUs); updateLoadControl(); return loadingFinished || !sampleQueue.isEmpty(); } @Override public int readData(int track, long positionUs, MediaFormatHolder formatHolder, SampleHolder sampleHolder, boolean onlyReadDiscontinuity) { Assertions.checkState(state == STATE_ENABLED); downstreamPositionUs = positionUs; if (pendingDiscontinuity) { pendingDiscontinuity = false; return DISCONTINUITY_READ; } if (onlyReadDiscontinuity) { return NOTHING_READ; } if (isPendingReset()) { return NOTHING_READ; } boolean haveSamples = !sampleQueue.isEmpty(); BaseMediaChunk currentChunk = mediaChunks.getFirst(); while (haveSamples && mediaChunks.size() > 1 && mediaChunks.get(1).getFirstSampleIndex() == sampleQueue.getReadIndex()) { mediaChunks.removeFirst(); currentChunk = mediaChunks.getFirst(); } if (downstreamFormat == null || !downstreamFormat.equals(currentChunk.format)) { notifyDownstreamFormatChanged(currentChunk.format, currentChunk.trigger, currentChunk.startTimeUs); downstreamFormat = currentChunk.format; } if (haveSamples || currentChunk.isMediaFormatFinal) { MediaFormat mediaFormat = currentChunk.getMediaFormat(); if (!mediaFormat.equals(downstreamMediaFormat)) { formatHolder.format = mediaFormat; formatHolder.drmInitData = currentChunk.getDrmInitData(); downstreamMediaFormat = mediaFormat; return FORMAT_READ; } } if (!haveSamples) { if (loadingFinished) { return END_OF_STREAM; } return NOTHING_READ; } if (sampleQueue.getSample(sampleHolder)) { boolean decodeOnly = sampleHolder.timeUs < lastSeekPositionUs; sampleHolder.flags |= decodeOnly ? C.SAMPLE_FLAG_DECODE_ONLY : 0; onSampleRead(currentChunk, sampleHolder); return SAMPLE_READ; } return NOTHING_READ; } @Override public void seekToUs(long positionUs) { Assertions.checkState(state == STATE_ENABLED); long currentPositionUs = isPendingReset() ? pendingResetPositionUs : downstreamPositionUs; downstreamPositionUs = positionUs; lastSeekPositionUs = positionUs; if (currentPositionUs == positionUs) { return; } // If we're not pending a reset, see if we can seek within the sample queue. boolean seekInsideBuffer = !isPendingReset() && sampleQueue.skipToKeyframeBefore(positionUs); if (seekInsideBuffer) { // We succeeded. All we need to do is discard any chunks that we've moved past. boolean haveSamples = !sampleQueue.isEmpty(); while (haveSamples && mediaChunks.size() > 1 && mediaChunks.get(1).getFirstSampleIndex() <= sampleQueue.getReadIndex()) { mediaChunks.removeFirst(); } } else { // We failed, and need to restart. restartFrom(positionUs); } // Either way, we need to send a discontinuity to the downstream components. pendingDiscontinuity = true; } @Override public void maybeThrowError() throws IOException { if (currentLoadableException != null && currentLoadableExceptionCount > minLoadableRetryCount) { throw currentLoadableException; } else if (currentLoadableHolder.chunk == null) { chunkSource.maybeThrowError(); } } @Override public long getBufferedPositionUs() { Assertions.checkState(state == STATE_ENABLED); if (isPendingReset()) { return pendingResetPositionUs; } else if (loadingFinished) { return TrackRenderer.END_OF_TRACK_US; } else { long largestParsedTimestampUs = sampleQueue.getLargestParsedTimestampUs(); return largestParsedTimestampUs == Long.MIN_VALUE ? downstreamPositionUs : largestParsedTimestampUs; } } @Override public void release() { Assertions.checkState(state != STATE_ENABLED); if (loader != null) { loader.release(); loader = null; } state = STATE_IDLE; } @Override public void onLoadCompleted(Loadable loadable) { long now = SystemClock.elapsedRealtime(); long loadDurationMs = now - currentLoadStartTimeMs; Chunk currentLoadable = currentLoadableHolder.chunk; chunkSource.onChunkLoadCompleted(currentLoadable); if (isMediaChunk(currentLoadable)) { BaseMediaChunk mediaChunk = (BaseMediaChunk) currentLoadable; notifyLoadCompleted(currentLoadable.bytesLoaded(), mediaChunk.type, mediaChunk.trigger, mediaChunk.format, mediaChunk.startTimeUs, mediaChunk.endTimeUs, now, loadDurationMs); } else { notifyLoadCompleted(currentLoadable.bytesLoaded(), currentLoadable.type, currentLoadable.trigger, currentLoadable.format, -1, -1, now, loadDurationMs); } clearCurrentLoadable(); updateLoadControl(); } @Override public void onLoadCanceled(Loadable loadable) { Chunk currentLoadable = currentLoadableHolder.chunk; notifyLoadCanceled(currentLoadable.bytesLoaded()); clearCurrentLoadable(); if (state == STATE_ENABLED) { restartFrom(pendingResetPositionUs); } else { sampleQueue.clear(); mediaChunks.clear(); clearCurrentLoadable(); loadControl.trimAllocator(); } } @Override public void onLoadError(Loadable loadable, IOException e) { currentLoadableException = e; currentLoadableExceptionCount++; currentLoadableExceptionTimestamp = SystemClock.elapsedRealtime(); notifyLoadError(e); chunkSource.onChunkLoadError(currentLoadableHolder.chunk, e); updateLoadControl(); } /** * Called when a sample has been read. Can be used to perform any modifications necessary before * the sample is returned. * * @param mediaChunk The chunk from which the sample was obtained. * @param sampleHolder Holds the read sample. */ protected void onSampleRead(MediaChunk mediaChunk, SampleHolder sampleHolder) { // Do nothing. } private void restartFrom(long positionUs) { pendingResetPositionUs = positionUs; loadingFinished = false; if (loader.isLoading()) { loader.cancelLoading(); } else { sampleQueue.clear(); mediaChunks.clear(); clearCurrentLoadable(); updateLoadControl(); } } private void clearCurrentLoadable() { currentLoadableHolder.chunk = null; clearCurrentLoadableException(); } private void clearCurrentLoadableException() { currentLoadableException = null; currentLoadableExceptionCount = 0; } private void updateLoadControl() { long now = SystemClock.elapsedRealtime(); long nextLoadPositionUs = getNextLoadPositionUs(); boolean isBackedOff = currentLoadableException != null; boolean loadingOrBackedOff = loader.isLoading() || isBackedOff; // If we're not loading or backed off, evaluate the operation if (a) we don't have the next // chunk yet and we're not finished, or (b) if the last evaluation was over 2000ms ago. if (!loadingOrBackedOff && ((currentLoadableHolder.chunk == null && nextLoadPositionUs != -1) || (now - lastPerformedBufferOperation > 2000))) { // Perform the evaluation. lastPerformedBufferOperation = now; doChunkOperation(); boolean chunksDiscarded = discardUpstreamMediaChunks(currentLoadableHolder.queueSize); // Update the next load position as appropriate. if (currentLoadableHolder.chunk == null) { // Set loadPosition to -1 to indicate that we don't have anything to load. nextLoadPositionUs = -1; } else if (chunksDiscarded) { // Chunks were discarded, so we need to re-evaluate the load position. nextLoadPositionUs = getNextLoadPositionUs(); } } // 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)) { resumeFromBackOff(); } return; } if (!loader.isLoading() && nextLoader) { maybeStartLoading(); } } /** * 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 : mediaChunks.getLast().endTimeUs; } } /** * Resumes loading. * <p> * If the {@link ChunkSource} returns a chunk equivalent to the backed off chunk B, then the * loading of B will be resumed. In all other cases B will be discarded and the new chunk will * be loaded. */ private void resumeFromBackOff() { currentLoadableException = null; Chunk backedOffChunk = currentLoadableHolder.chunk; if (!isMediaChunk(backedOffChunk)) { doChunkOperation(); discardUpstreamMediaChunks(currentLoadableHolder.queueSize); if (currentLoadableHolder.chunk == backedOffChunk) { // Chunk was unchanged. Resume loading. loader.startLoading(backedOffChunk, this); } else { // Chunk was changed. Notify that the existing load was canceled. notifyLoadCanceled(backedOffChunk.bytesLoaded()); // Start loading the replacement. maybeStartLoading(); } return; } if (backedOffChunk == mediaChunks.getFirst()) { // We're not able to clear the first media chunk, so we have no choice but to continue // loading it. loader.startLoading(backedOffChunk, this); return; } // The current loadable is the last media chunk. Remove it before we invoke the chunk source, // and add it back again afterwards. BaseMediaChunk removedChunk = mediaChunks.removeLast(); Assertions.checkState(backedOffChunk == removedChunk); doChunkOperation(); mediaChunks.add(removedChunk); if (currentLoadableHolder.chunk == backedOffChunk) { // Chunk was unchanged. Resume loading. loader.startLoading(backedOffChunk, this); } else { // Chunk was changed. Notify that the existing load was canceled. notifyLoadCanceled(backedOffChunk.bytesLoaded()); // This call will remove and release at least one chunk from the end of mediaChunks. Since // the current loadable is the last media chunk, it is guaranteed to be removed. discardUpstreamMediaChunks(currentLoadableHolder.queueSize); clearCurrentLoadableException(); maybeStartLoading(); } } private void maybeStartLoading() { Chunk currentLoadable = currentLoadableHolder.chunk; if (currentLoadable == null) { // Nothing to load. return; } currentLoadStartTimeMs = SystemClock.elapsedRealtime(); if (isMediaChunk(currentLoadable)) { BaseMediaChunk mediaChunk = (BaseMediaChunk) currentLoadable; mediaChunk.init(sampleQueue); mediaChunks.add(mediaChunk); if (isPendingReset()) { pendingResetPositionUs = NO_RESET_PENDING; } notifyLoadStarted(mediaChunk.dataSpec.length, mediaChunk.type, mediaChunk.trigger, mediaChunk.format, mediaChunk.startTimeUs, mediaChunk.endTimeUs); } else { notifyLoadStarted(currentLoadable.dataSpec.length, currentLoadable.type, currentLoadable.trigger, currentLoadable.format, -1, -1); } loader.startLoading(currentLoadable, this); } /** * Sets up the {@link #currentLoadableHolder}, passes it to the chunk source to cause it to be * updated with the next operation, and updates {@link #loadingFinished} if the end of the stream * is reached. */ private void doChunkOperation() { currentLoadableHolder.endOfStream = false; currentLoadableHolder.queueSize = readOnlyMediaChunks.size(); chunkSource.getChunkOperation(readOnlyMediaChunks, pendingResetPositionUs, downstreamPositionUs, currentLoadableHolder); loadingFinished = currentLoadableHolder.endOfStream; } /** * Discard upstream media chunks until the queue length is equal to the length specified. * * @param queueLength The desired length of the queue. * @return True if chunks were discarded. False otherwise. */ private boolean discardUpstreamMediaChunks(int queueLength) { if (mediaChunks.size() <= queueLength) { return false; } long startTimeUs = 0; long endTimeUs = mediaChunks.getLast().endTimeUs; BaseMediaChunk removed = null; while (mediaChunks.size() > queueLength) { removed = mediaChunks.removeLast(); startTimeUs = removed.startTimeUs; } sampleQueue.discardUpstreamSamples(removed.getFirstSampleIndex()); notifyUpstreamDiscarded(startTimeUs, endTimeUs); return true; } private boolean isMediaChunk(Chunk chunk) { return chunk instanceof BaseMediaChunk; } private boolean isPendingReset() { return pendingResetPositionUs != NO_RESET_PENDING; } private long getRetryDelayMillis(long errorCount) { return Math.min((errorCount - 1) * 1000, 5000); } protected final 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 notifyUpstreamDiscarded(final long mediaStartTimeUs, final long mediaEndTimeUs) { if (eventHandler != null && eventListener != null) { eventHandler.post(new Runnable() { @Override public void run() { eventListener.onUpstreamDiscarded(eventSourceId, usToMs(mediaStartTimeUs), usToMs(mediaEndTimeUs)); } }); } } 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)); } }); } } }