/* * 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.TrackInfo; import com.google.android.exoplayer.TrackRenderer; import com.google.android.exoplayer.upstream.Loader; 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.Iterator; 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, Loader.Listener { /** * Interface definition for a callback to be notified of {@link ChunkSampleSource} events. */ public interface EventListener { /** * Invoked when an upstream load is started. * * @param sourceId The id of the reporting {@link SampleSource}. * @param formatId The format id. * @param trigger A trigger for the format selection, as specified by the {@link ChunkSource}. * @param isInitialization Whether the load is for format initialization data. * @param mediaStartTimeMs The media time of the start of the data being loaded, or -1 if this * load is for initialization data. * @param mediaEndTimeMs The media time of the end of the data being loaded, or -1 if this * load is for initialization data. * @param length The length of the data being loaded in bytes, or {@link C#LENGTH_UNBOUNDED} if * the length of the data has not yet been determined. */ void onLoadStarted(int sourceId, String formatId, int trigger, boolean isInitialization, int mediaStartTimeMs, int mediaEndTimeMs, long length); /** * Invoked when the current load operation completes. * * @param sourceId The id of the reporting {@link SampleSource}. * @param bytesLoaded The number of bytes that were loaded. */ void onLoadCompleted(int sourceId, long bytesLoaded); /** * Invoked when the current upstream load operation is canceled. * * @param sourceId The id of the reporting {@link SampleSource}. * @param bytesLoaded The number of bytes that were loaded prior to the cancellation. */ void onLoadCanceled(int sourceId, long bytesLoaded); /** * Invoked when data is removed from the back of the buffer, typically so that it can be * re-buffered using a different representation. * * @param sourceId The id of the reporting {@link SampleSource}. * @param mediaStartTimeMs The media time of the start of the discarded data. * @param mediaEndTimeMs The media time of the end of the discarded data. * @param bytesDiscarded The length of the data being discarded in bytes. */ void onUpstreamDiscarded(int sourceId, int mediaStartTimeMs, int mediaEndTimeMs, long bytesDiscarded); /** * Invoked when an error occurs loading media data. * * @param sourceId The id of the reporting {@link SampleSource}. * @param e The cause of the failure. */ void onUpstreamError(int sourceId, IOException e); /** * Invoked when an error occurs consuming loaded data. * * @param sourceId The id of the reporting {@link SampleSource}. * @param e The cause of the failure. */ void onConsumptionError(int sourceId, IOException e); /** * Invoked when data is removed from the front of the buffer, typically due to a seek or * because the data has been consumed. * * @param sourceId The id of the reporting {@link SampleSource}. * @param mediaStartTimeMs The media time of the start of the discarded data. * @param mediaEndTimeMs The media time of the end of the discarded data. * @param bytesDiscarded The length of the data being discarded in bytes. */ void onDownstreamDiscarded(int sourceId, int mediaStartTimeMs, int mediaEndTimeMs, long bytesDiscarded); /** * Invoked when the downstream format changes (i.e. when the format being supplied to the * caller of {@link SampleSource#readData} changes). * * @param sourceId The id of the reporting {@link SampleSource}. * @param formatId The format id. * @param trigger The trigger specified in the corresponding upstream load, as specified by the * {@link ChunkSource}. * @param mediaTimeMs The media time at which the change occurred. */ void onDownstreamFormatChanged(int sourceId, String formatId, int trigger, int mediaTimeMs); } private static final int STATE_UNPREPARED = 0; private static final int STATE_PREPARED = 1; private static final int STATE_ENABLED = 2; private static final int NO_RESET_PENDING = -1; private final int eventSourceId; private final LoadControl loadControl; private final ChunkSource chunkSource; private final ChunkOperationHolder currentLoadableHolder; private final LinkedList<MediaChunk> mediaChunks; private final List<MediaChunk> readOnlyMediaChunks; private final int bufferSizeContribution; private final boolean frameAccurateSeeking; private final Handler eventHandler; private final EventListener eventListener; private int state; private long downstreamPositionUs; private long lastSeekPositionUs; private long pendingResetTime; private long lastPerformedBufferOperation; private boolean pendingDiscontinuity; private Loader loader; private IOException currentLoadableException; private boolean currentLoadableExceptionFatal; private int currentLoadableExceptionCount; private long currentLoadableExceptionTimestamp; private MediaFormat downstreamMediaFormat; private volatile Format downstreamFormat; public ChunkSampleSource(ChunkSource chunkSource, LoadControl loadControl, int bufferSizeContribution, boolean frameAccurateSeeking) { this(chunkSource, loadControl, bufferSizeContribution, frameAccurateSeeking, null, null, 0); } public ChunkSampleSource(ChunkSource chunkSource, LoadControl loadControl, int bufferSizeContribution, boolean frameAccurateSeeking, Handler eventHandler, EventListener eventListener, int eventSourceId) { this.chunkSource = chunkSource; this.loadControl = loadControl; this.bufferSizeContribution = bufferSizeContribution; this.frameAccurateSeeking = frameAccurateSeeking; this.eventHandler = eventHandler; this.eventListener = eventListener; this.eventSourceId = eventSourceId; currentLoadableHolder = new ChunkOperationHolder(); mediaChunks = new LinkedList<MediaChunk>(); readOnlyMediaChunks = Collections.unmodifiableList(mediaChunks); state = STATE_UNPREPARED; } /** * Exposes the current downstream format for debugging purposes. Can be called from any thread. * * @return The current downstream format. */ public Format getFormat() { return downstreamFormat; } @Override public boolean prepare() { Assertions.checkState(state == STATE_UNPREPARED); loader = new Loader("Loader:" + chunkSource.getTrackInfo().mimeType, this); state = STATE_PREPARED; return true; } @Override public int getTrackCount() { Assertions.checkState(state != STATE_UNPREPARED); return 1; } @Override public TrackInfo getTrackInfo(int track) { Assertions.checkState(state != STATE_UNPREPARED); Assertions.checkState(track == 0); return chunkSource.getTrackInfo(); } @Override public void enable(int track, long timeUs) { Assertions.checkState(state == STATE_PREPARED); Assertions.checkState(track == 0); state = STATE_ENABLED; chunkSource.enable(); loadControl.register(this, bufferSizeContribution); downstreamFormat = null; downstreamMediaFormat = null; downstreamPositionUs = timeUs; lastSeekPositionUs = timeUs; restartFrom(timeUs); } @Override public void disable(int track) { Assertions.checkState(state == STATE_ENABLED); Assertions.checkState(track == 0); pendingDiscontinuity = false; state = STATE_PREPARED; loadControl.unregister(this); chunkSource.disable(mediaChunks); if (loader.isLoading()) { loader.cancelLoading(); } else { clearMediaChunks(); clearCurrentLoadable(); loadControl.trimAllocator(); } } @Override public boolean continueBuffering(long playbackPositionUs) throws IOException { Assertions.checkState(state == STATE_ENABLED); downstreamPositionUs = playbackPositionUs; chunkSource.continueBuffering(playbackPositionUs); updateLoadControl(); if (isPendingReset() || mediaChunks.isEmpty()) { 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(state == STATE_ENABLED); Assertions.checkState(track == 0); if (pendingDiscontinuity) { pendingDiscontinuity = false; return DISCONTINUITY_READ; } if (onlyReadDiscontinuity) { return NOTHING_READ; } downstreamPositionUs = playbackPositionUs; if (isPendingReset()) { if (currentLoadableException != null) { throw currentLoadableException; } IOException chunkSourceException = chunkSource.getError(); if (chunkSourceException != null) { throw chunkSourceException; } return NOTHING_READ; } MediaChunk mediaChunk = mediaChunks.getFirst(); if (mediaChunk.isReadFinished()) { // We've read all of the samples from the current media chunk. if (mediaChunks.size() > 1) { discardDownstreamMediaChunk(); mediaChunk = mediaChunks.getFirst(); mediaChunk.seekToStart(); return readData(track, playbackPositionUs, formatHolder, sampleHolder, false); } else if (mediaChunk.isLastChunk()) { return END_OF_STREAM; } IOException chunkSourceException = chunkSource.getError(); if (chunkSourceException != null) { throw chunkSourceException; } return NOTHING_READ; } if (downstreamFormat == null || !downstreamFormat.equals(mediaChunk.format)) { notifyDownstreamFormatChanged(mediaChunk.format.id, mediaChunk.trigger, mediaChunk.startTimeUs); downstreamFormat = mediaChunk.format; } if (!mediaChunk.prepare()) { if (currentLoadableException != null) { throw currentLoadableException; } return NOTHING_READ; } MediaFormat mediaFormat = mediaChunk.getMediaFormat(); if (mediaFormat != null && !mediaFormat.equals(downstreamMediaFormat, true)) { chunkSource.getMaxVideoDimensions(mediaFormat); formatHolder.format = mediaFormat; formatHolder.drmInitData = mediaChunk.getPsshInfo(); downstreamMediaFormat = mediaFormat; return FORMAT_READ; } if (mediaChunk.read(sampleHolder)) { sampleHolder.decodeOnly = frameAccurateSeeking && sampleHolder.timeUs < lastSeekPositionUs; onSampleRead(mediaChunk, sampleHolder); return SAMPLE_READ; } else { if (currentLoadableException != null) { throw currentLoadableException; } return NOTHING_READ; } } @Override public void seekToUs(long timeUs) { Assertions.checkState(state == STATE_ENABLED); downstreamPositionUs = timeUs; lastSeekPositionUs = timeUs; if (pendingResetTime == timeUs) { return; } MediaChunk mediaChunk = getMediaChunk(timeUs); if (mediaChunk == null) { restartFrom(timeUs); pendingDiscontinuity = true; } else { pendingDiscontinuity |= mediaChunk.seekTo(timeUs, mediaChunk == mediaChunks.getFirst()); discardDownstreamMediaChunks(mediaChunk); updateLoadControl(); } } private MediaChunk getMediaChunk(long timeUs) { Iterator<MediaChunk> mediaChunkIterator = mediaChunks.iterator(); while (mediaChunkIterator.hasNext()) { MediaChunk mediaChunk = mediaChunkIterator.next(); if (timeUs < mediaChunk.startTimeUs) { return null; } else if (mediaChunk.isLastChunk() || timeUs < mediaChunk.endTimeUs) { return mediaChunk; } } return null; } @Override public long getBufferedPositionUs() { Assertions.checkState(state == STATE_ENABLED); if (isPendingReset()) { return pendingResetTime; } MediaChunk mediaChunk = mediaChunks.getLast(); Chunk currentLoadable = currentLoadableHolder.chunk; if (currentLoadable != null && mediaChunk == currentLoadable) { // Linearly interpolate partially-fetched chunk times. long chunkLength = mediaChunk.getLength(); if (chunkLength != C.LENGTH_UNBOUNDED) { return mediaChunk.startTimeUs + ((mediaChunk.endTimeUs - mediaChunk.startTimeUs) * mediaChunk.bytesLoaded()) / chunkLength; } else { return mediaChunk.startTimeUs; } } else if (mediaChunk.isLastChunk()) { return TrackRenderer.END_OF_TRACK_US; } else { return mediaChunk.endTimeUs; } } @Override public void release() { Assertions.checkState(state != STATE_ENABLED); if (loader != null) { loader.release(); loader = null; } state = STATE_UNPREPARED; } @Override public void onLoaded() { Chunk currentLoadable = currentLoadableHolder.chunk; notifyLoadCompleted(currentLoadable.bytesLoaded()); try { currentLoadable.consume(); } catch (IOException e) { currentLoadableException = e; currentLoadableExceptionCount++; currentLoadableExceptionTimestamp = SystemClock.elapsedRealtime(); currentLoadableExceptionFatal = true; notifyConsumptionError(e); } finally { if (!isMediaChunk(currentLoadable)) { currentLoadable.release(); } if (!currentLoadableExceptionFatal) { clearCurrentLoadable(); } updateLoadControl(); } } @Override public void onCanceled() { Chunk currentLoadable = currentLoadableHolder.chunk; notifyLoadCanceled(currentLoadable.bytesLoaded()); if (!isMediaChunk(currentLoadable)) { currentLoadable.release(); } clearCurrentLoadable(); if (state == STATE_ENABLED) { restartFrom(pendingResetTime); } else { clearMediaChunks(); loadControl.trimAllocator(); } } @Override public void onError(IOException e) { currentLoadableException = e; currentLoadableExceptionCount++; currentLoadableExceptionTimestamp = SystemClock.elapsedRealtime(); notifyUpstreamError(e); chunkSource.onChunkLoadError(currentLoadableHolder.chunk, e); updateLoadControl(); } /** * Called when a sample has been read from a {@link MediaChunk}. Can be used to perform any * modifications necessary before the sample is returned. * * @param mediaChunk The MediaChunk the sample was ready from. * @param sampleHolder The sample that has just been read. */ protected void onSampleRead(MediaChunk mediaChunk, SampleHolder sampleHolder) { // no-op } private void restartFrom(long timeUs) { pendingResetTime = timeUs; if (loader.isLoading()) { loader.cancelLoading(); } else { clearMediaChunks(); clearCurrentLoadable(); updateLoadControl(); } } private void clearMediaChunks() { discardDownstreamMediaChunks(null); } private void clearCurrentLoadable() { currentLoadableHolder.chunk = null; currentLoadableException = null; currentLoadableExceptionCount = 0; currentLoadableExceptionFatal = false; } private void updateLoadControl() { long loadPositionUs; if (isPendingReset()) { loadPositionUs = pendingResetTime; } else { MediaChunk lastMediaChunk = mediaChunks.getLast(); loadPositionUs = lastMediaChunk.nextChunkIndex == -1 ? -1 : lastMediaChunk.endTimeUs; } boolean isBackedOff = currentLoadableException != null && !currentLoadableExceptionFatal; boolean nextLoader = loadControl.update(this, downstreamPositionUs, loadPositionUs, isBackedOff || loader.isLoading(), currentLoadableExceptionFatal); if (currentLoadableExceptionFatal) { return; } long now = SystemClock.elapsedRealtime(); if (isBackedOff) { long elapsedMillis = now - currentLoadableExceptionTimestamp; if (elapsedMillis >= getRetryDelayMillis(currentLoadableExceptionCount)) { resumeFromBackOff(); } return; } if (!loader.isLoading()) { if (currentLoadableHolder.chunk == null || now - lastPerformedBufferOperation > 1000) { lastPerformedBufferOperation = now; currentLoadableHolder.queueSize = readOnlyMediaChunks.size(); chunkSource.getChunkOperation(readOnlyMediaChunks, pendingResetTime, downstreamPositionUs, currentLoadableHolder); discardUpstreamMediaChunks(currentLoadableHolder.queueSize); } if (nextLoader) { maybeStartLoading(); } } } /** * 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)) { currentLoadableHolder.queueSize = readOnlyMediaChunks.size(); chunkSource.getChunkOperation(readOnlyMediaChunks, pendingResetTime, downstreamPositionUs, currentLoadableHolder); discardUpstreamMediaChunks(currentLoadableHolder.queueSize); if (currentLoadableHolder.chunk == backedOffChunk) { // Chunk was unchanged. Resume loading. loader.startLoading(backedOffChunk); } else { backedOffChunk.release(); 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); return; } // The current loadable is the last media chunk. Remove it before we invoke the chunk source, // and add it back again afterwards. MediaChunk removedChunk = mediaChunks.removeLast(); Assertions.checkState(backedOffChunk == removedChunk); currentLoadableHolder.queueSize = readOnlyMediaChunks.size(); chunkSource.getChunkOperation(readOnlyMediaChunks, pendingResetTime, downstreamPositionUs, currentLoadableHolder); mediaChunks.add(removedChunk); if (currentLoadableHolder.chunk == backedOffChunk) { // Chunk was unchanged. Resume loading. loader.startLoading(backedOffChunk); } else { // 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); clearCurrentLoadable(); maybeStartLoading(); } } private void maybeStartLoading() { Chunk currentLoadable = currentLoadableHolder.chunk; if (currentLoadable == null) { // Nothing to load. return; } currentLoadable.init(loadControl.getAllocator()); if (isMediaChunk(currentLoadable)) { MediaChunk mediaChunk = (MediaChunk) currentLoadable; if (isPendingReset()) { mediaChunk.seekTo(pendingResetTime, false); pendingResetTime = NO_RESET_PENDING; } mediaChunks.add(mediaChunk); notifyLoadStarted(mediaChunk.format.id, mediaChunk.trigger, false, mediaChunk.startTimeUs, mediaChunk.endTimeUs, mediaChunk.getLength()); } else { notifyLoadStarted(currentLoadable.format.id, currentLoadable.trigger, true, -1, -1, currentLoadable.getLength()); } loader.startLoading(currentLoadable); } /** * Discards downstream media chunks until {@code untilChunk} if found. {@code untilChunk} is not * itself discarded. Null can be passed to discard all media chunks. * * @param untilChunk The first media chunk to keep, or null to discard all media chunks. */ private void discardDownstreamMediaChunks(MediaChunk untilChunk) { if (mediaChunks.isEmpty() || untilChunk == mediaChunks.getFirst()) { return; } long totalBytes = 0; long startTimeUs = mediaChunks.getFirst().startTimeUs; long endTimeUs = 0; while (!mediaChunks.isEmpty() && untilChunk != mediaChunks.getFirst()) { MediaChunk removed = mediaChunks.removeFirst(); totalBytes += removed.bytesLoaded(); endTimeUs = removed.endTimeUs; removed.release(); } notifyDownstreamDiscarded(startTimeUs, endTimeUs, totalBytes); } /** * Discards the first downstream media chunk. */ private void discardDownstreamMediaChunk() { MediaChunk removed = mediaChunks.removeFirst(); long totalBytes = removed.bytesLoaded(); removed.release(); notifyDownstreamDiscarded(removed.startTimeUs, removed.endTimeUs, totalBytes); } /** * Discard upstream media chunks until the queue length is equal to the length specified. * * @param queueLength The desired length of the queue. */ private void discardUpstreamMediaChunks(int queueLength) { if (mediaChunks.size() <= queueLength) { return; } long totalBytes = 0; long startTimeUs = 0; long endTimeUs = mediaChunks.getLast().endTimeUs; while (mediaChunks.size() > queueLength) { MediaChunk removed = mediaChunks.removeLast(); totalBytes += removed.bytesLoaded(); startTimeUs = removed.startTimeUs; removed.release(); } notifyUpstreamDiscarded(startTimeUs, endTimeUs, totalBytes); } private boolean isMediaChunk(Chunk chunk) { return chunk instanceof MediaChunk; } private boolean isPendingReset() { return pendingResetTime != 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 String formatId, final int trigger, final boolean isInitialization, final long mediaStartTimeUs, final long mediaEndTimeUs, final long length) { if (eventHandler != null && eventListener != null) { eventHandler.post(new Runnable() { @Override public void run() { eventListener.onLoadStarted(eventSourceId, formatId, trigger, isInitialization, usToMs(mediaStartTimeUs), usToMs(mediaEndTimeUs), length); } }); } } private void notifyLoadCompleted(final long bytesLoaded) { if (eventHandler != null && eventListener != null) { eventHandler.post(new Runnable() { @Override public void run() { eventListener.onLoadCompleted(eventSourceId, bytesLoaded); } }); } } 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 notifyUpstreamError(final IOException e) { if (eventHandler != null && eventListener != null) { eventHandler.post(new Runnable() { @Override public void run() { eventListener.onUpstreamError(eventSourceId, e); } }); } } private void notifyConsumptionError(final IOException e) { if (eventHandler != null && eventListener != null) { eventHandler.post(new Runnable() { @Override public void run() { eventListener.onConsumptionError(eventSourceId, e); } }); } } private void notifyUpstreamDiscarded(final long mediaStartTimeUs, final long mediaEndTimeUs, final long totalBytes) { if (eventHandler != null && eventListener != null) { eventHandler.post(new Runnable() { @Override public void run() { eventListener.onUpstreamDiscarded(eventSourceId, usToMs(mediaStartTimeUs), usToMs(mediaEndTimeUs), totalBytes); } }); } } private void notifyDownstreamFormatChanged(final String formatId, final int trigger, final long mediaTimeUs) { if (eventHandler != null && eventListener != null) { eventHandler.post(new Runnable() { @Override public void run() { eventListener.onDownstreamFormatChanged(eventSourceId, formatId, trigger, usToMs(mediaTimeUs)); } }); } } private void notifyDownstreamDiscarded(final long mediaStartTimeUs, final long mediaEndTimeUs, final long bytesDiscarded) { if (eventHandler != null && eventListener != null) { eventHandler.post(new Runnable() { @Override public void run() { eventListener.onDownstreamDiscarded(eventSourceId, usToMs(mediaStartTimeUs), usToMs(mediaEndTimeUs), bytesDiscarded); } }); } } }