From ca81444f95437b754e6f807c85290f2a184fd4dc Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 29 Jun 2016 02:55:31 -0700 Subject: [PATCH] Event based preparation. - Removes the load delay that was previously present after source preparation. - Prevents premature failure in the case that the buffering source fails to prepare. - SampleSource.Callback will get a second method in a subsequent CL, approximately meaning "tell me if I can load more stuff". This will remove the need for LoadControl and the complexity that comes with it. ------------- Created by MOE: https://github.com/google/moe MOE_MIGRATED_REVID=126172573 --- .../exoplayer/ExoPlayerImplInternal.java | 150 ++++++++++-------- .../android/exoplayer/MultiSampleSource.java | 81 ++++++---- .../android/exoplayer/SampleSource.java | 49 ++++-- .../android/exoplayer/SingleSampleSource.java | 10 +- .../android/exoplayer/TrackRenderer.java | 4 + .../exoplayer/chunk/ChunkTrackStream.java | 3 +- .../exoplayer/dash/DashSampleSource.java | 24 +-- .../extractor/DefaultTrackOutput.java | 30 +++- .../extractor/ExtractorSampleSource.java | 84 +++++----- .../exoplayer/hls/HlsSampleSource.java | 113 +++++++------ .../exoplayer/hls/HlsTrackStreamWrapper.java | 92 ++++++----- .../SmoothStreamingSampleSource.java | 17 +- .../android/exoplayer/upstream/Loader.java | 4 +- 13 files changed, 401 insertions(+), 260 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer/ExoPlayerImplInternal.java b/library/src/main/java/com/google/android/exoplayer/ExoPlayerImplInternal.java index 104d7ca3ea..97e0a2d21a 100644 --- a/library/src/main/java/com/google/android/exoplayer/ExoPlayerImplInternal.java +++ b/library/src/main/java/com/google/android/exoplayer/ExoPlayerImplInternal.java @@ -15,7 +15,6 @@ */ package com.google.android.exoplayer; -import com.google.android.exoplayer.BufferingPolicy.LoadControl; import com.google.android.exoplayer.ExoPlayer.ExoPlayerMessage; import com.google.android.exoplayer.TrackSelector.InvalidationListener; import com.google.android.exoplayer.util.PriorityHandlerThread; @@ -38,7 +37,8 @@ import java.util.ArrayList; */ // TODO[REFACTOR]: Make sure renderer errors that will prevent prepare from being called again are // always propagated properly. -/* package */ final class ExoPlayerImplInternal implements Handler.Callback, InvalidationListener { +/* package */ final class ExoPlayerImplInternal implements Handler.Callback, SampleSource.Callback, + InvalidationListener { /** * Playback position information which is read on the application's thread by @@ -76,8 +76,9 @@ import java.util.ArrayList; private static final int MSG_SEEK_TO = 3; private static final int MSG_STOP = 4; private static final int MSG_RELEASE = 5; - private static final int MSG_TRACK_SELECTION_INVALIDATED = 6; - private static final int MSG_CUSTOM = 7; + private static final int MSG_SOURCE_PREPARED = 6; + private static final int MSG_TRACK_SELECTION_INVALIDATED = 7; + private static final int MSG_CUSTOM = 8; private static final int PREPARING_SOURCE_INTERVAL_MS = 10; private static final int RENDERING_INTERVAL_MS = 10; @@ -203,6 +204,13 @@ import java.util.ArrayList; handler.sendEmptyMessage(MSG_TRACK_SELECTION_INVALIDATED); } + // SampleSource.Callback implementation. + + @Override + public void onSourcePrepared(SampleSource source) { + handler.obtainMessage(MSG_SOURCE_PREPARED, source).sendToTarget(); + } + // Handler.Callback implementation. @Override @@ -233,14 +241,18 @@ import java.util.ArrayList; releaseInternal(); return true; } - case MSG_CUSTOM: { - sendMessagesInternal((ExoPlayerMessage[]) msg.obj); + case MSG_SOURCE_PREPARED: { + timeline.handleSourcePrepared((SampleSource) msg.obj); return true; } case MSG_TRACK_SELECTION_INVALIDATED: { reselectTracksInternal(); return true; } + case MSG_CUSTOM: { + sendMessagesInternal((ExoPlayerMessage[]) msg.obj); + return true; + } default: return false; } @@ -368,7 +380,8 @@ import java.util.ArrayList; timeline.updateSources(); if (timeline.getSampleSource() == null) { - // We're still waiting for the source to be prepared. + // We're still waiting for the first source to be prepared. + timeline.maybeThrowSourcePrepareError(); scheduleNextOperation(MSG_DO_SOME_WORK, operationStartTimeMs, PREPARING_SOURCE_INTERVAL_MS); return; } @@ -393,15 +406,16 @@ import java.util.ArrayList; allRenderersReadyOrEnded = allRenderersReadyOrEnded && rendererReadyOrEnded; } - // TODO: Have timeline.updateSources() above return whether the timeline is ready, and remove - // timeline.isReady(). This will avoid any inconsistencies that could arise due to the playback - // position update. We could probably return [ENDED|READY|BUFFERING] and get rid of isEnded too. + if (!allRenderersReadyOrEnded) { + timeline.maybeThrowSourcePrepareError(); + } + if (allRenderersEnded && (playbackInfo.durationUs == C.UNSET_TIME_US - || playbackInfo.durationUs <= playbackInfo.positionUs) && timeline.isEnded()) { + || playbackInfo.durationUs <= playbackInfo.positionUs) && timeline.isEnded) { setState(ExoPlayer.STATE_ENDED); stopRenderers(); } else if (state == ExoPlayer.STATE_BUFFERING) { - if ((enabledRenderers.length > 0 ? allRenderersReadyOrEnded : timeline.isReady()) + if ((enabledRenderers.length > 0 ? allRenderersReadyOrEnded : timeline.isReady) && bufferingPolicy.haveSufficientBuffer(playbackInfo.bufferedPositionUs, rebuffering)) { setState(ExoPlayer.STATE_READY); if (playWhenReady) { @@ -409,7 +423,7 @@ import java.util.ArrayList; } } } else if (state == ExoPlayer.STATE_READY) { - if (enabledRenderers.length > 0 ? !allRenderersReadyOrEnded : !timeline.isReady()) { + if (enabledRenderers.length > 0 ? !allRenderersReadyOrEnded : !timeline.isReady) { rebuffering = playWhenReady; setState(ExoPlayer.STATE_BUFFERING); stopRenderers(); @@ -558,6 +572,9 @@ import java.util.ArrayList; private final ArrayList oldStreams; private final ArrayList newSelections; + public boolean isReady; + public boolean isEnded; + private Source playingSource; private Source readingSource; private Source bufferingSource; @@ -577,19 +594,16 @@ import java.util.ArrayList; return playingSource == null ? null : playingSource.sampleSource; } - public boolean isEnded() { - if (playingSource == null) { - return false; + public void maybeThrowSourcePrepareError() throws IOException { + if (bufferingSource != null && !bufferingSource.prepared + && (readingSource == null || readingSource.nextSource == bufferingSource)) { + for (TrackRenderer renderer : enabledRenderers) { + if (!renderer.hasReadStreamToEnd()) { + return; + } + } + bufferingSource.sampleSource.maybeThrowPrepareError(); } - int sourceCount = sampleSourceProvider.getSourceCount(); - return sourceCount != SampleSourceProvider.UNKNOWN_SOURCE_COUNT - && playingSource.index == sourceCount - 1; - } - - public boolean isReady() { - return playingSourceEndPositionUs == C.UNSET_TIME_US - || internalPositionUs < playingSourceEndPositionUs - || (playingSource.nextSource != null && playingSource.nextSource.prepared); } public void updateSources() throws ExoPlaybackException, IOException { @@ -611,36 +625,24 @@ import java.util.ArrayList; bufferingSource.setNextSource(newSource); } bufferingSource = newSource; + long startPositionUs = playingSource == null ? playbackInfo.positionUs : 0; + sampleSource.prepare(ExoPlayerImplInternal.this, bufferingPolicy.getLoadControl(), + startPositionUs); } } } - if (bufferingSource != null) { - if (!bufferingSource.prepared) { - // Continue preparation. - // TODO[playlists]: Add support for setting the start position to play in a source. - long startPositionUs = playingSource == null ? playbackInfo.positionUs : 0; - if (bufferingSource.prepare(startPositionUs, bufferingPolicy.getLoadControl())) { - Pair result = trackSelector.selectTracks(renderers, - bufferingSource.sampleSource.getTrackGroups()); - bufferingSource.selectTracks(result.first, result.second, startPositionUs, - bufferingPolicy, renderers); - if (playingSource == null) { - // This is the first prepared source, so start playing it. - readingSource = bufferingSource; - setPlayingSource(readingSource); - } - } - } - if (bufferingSource.hasEnabledTracks) { - long sourcePositionUs = internalPositionUs - bufferingSource.offsetUs; - bufferingSource.sampleSource.continueBuffering(sourcePositionUs); - } - } - // Update the playing and reading sources. + if (bufferingSource != null && bufferingSource.hasEnabledTracks) { + long sourcePositionUs = internalPositionUs - bufferingSource.offsetUs; + bufferingSource.sampleSource.continueBuffering(sourcePositionUs); + } + if (playingSource == null) { + // We're waiting for the first source to be prepared. return; } + + // Update the playing and reading sources. if (playingSourceEndPositionUs == C.UNSET_TIME_US && playingSource.isFullyBuffered()) { playingSourceEndPositionUs = playingSource.offsetUs + playingSource.sampleSource.getDurationUs(); @@ -656,6 +658,7 @@ import java.util.ArrayList; updatePlaybackPositions(); eventHandler.obtainMessage(MSG_SOURCE_CHANGED, playbackInfo).sendToTarget(); } + updateTimelineState(); if (readingSource == null) { return; } @@ -698,6 +701,23 @@ import java.util.ArrayList; } } + public void handleSourcePrepared(SampleSource sampleSource) throws ExoPlaybackException { + if (bufferingSource == null || bufferingSource.sampleSource != sampleSource) { + // Stale event. + return; + } + long startPositionUs = playingSource == null ? playbackInfo.positionUs : 0; + Pair result = trackSelector.selectTracks(renderers, + bufferingSource.sampleSource.getTrackGroups()); + bufferingSource.handlePrepared(result.first, result.second, startPositionUs, + bufferingPolicy, renderers); + if (playingSource == null) { + // This is the first prepared source, so start playing it. + setPlayingSource(bufferingSource); + updateTimelineState(); + } + } + public void seekToSource(int sourceIndex) throws ExoPlaybackException { // Clear the timeline, but keep the requested source if it is already prepared. Source source = playingSource; @@ -714,6 +734,7 @@ import java.util.ArrayList; if (newPlayingSource != null) { newPlayingSource.nextSource = null; setPlayingSource(newPlayingSource); + updateTimelineState(); readingSource = playingSource; bufferingSource = playingSource; } else { @@ -794,6 +815,8 @@ import java.util.ArrayList; source.release(); source = source.nextSource; } + isReady = false; + isEnded = false; playingSource = null; readingSource = null; bufferingSource = null; @@ -812,6 +835,15 @@ import java.util.ArrayList; enableRenderers(source.trackSelections, enabledRendererCount); } + private void updateTimelineState() { + isReady = playingSourceEndPositionUs == C.UNSET_TIME_US + || internalPositionUs < playingSourceEndPositionUs + || (playingSource.nextSource != null && playingSource.nextSource.prepared); + int sourceCount = sampleSourceProvider.getSourceCount(); + isEnded = sourceCount != SampleSourceProvider.UNKNOWN_SOURCE_COUNT + && playingSource.index == sourceCount - 1; + } + private int disableRenderers(boolean sourceTransition, TrackSelectionArray newTrackSelections) throws ExoPlaybackException { // Disable any renderers whose selections have changed, adding the corresponding TrackStream @@ -924,28 +956,20 @@ import java.util.ArrayList; trackStreams = new TrackStream[rendererCount]; } - public boolean isFullyBuffered() { - return prepared && (!hasEnabledTracks - || sampleSource.getBufferedPositionUs() == C.END_OF_SOURCE_US); - } - - public boolean prepare(long startPositionUs, LoadControl loadControl) throws IOException { - if (sampleSource.prepare(startPositionUs, loadControl)) { - prepared = true; - return true; - } else { - return false; - } - } - public void setNextSource(Source nextSource) { this.nextSource = nextSource; nextSource.offsetUs = offsetUs + sampleSource.getDurationUs(); } - public void selectTracks(TrackSelectionArray newTrackSelections, Object trackSelectionData, + public boolean isFullyBuffered() { + return prepared && (!hasEnabledTracks + || sampleSource.getBufferedPositionUs() == C.END_OF_SOURCE_US); + } + + public void handlePrepared(TrackSelectionArray newTrackSelections, Object trackSelectionData, long positionUs, BufferingPolicy bufferingPolicy, TrackRenderer[] renderers) throws ExoPlaybackException { + prepared = true; this.trackSelectionData = trackSelectionData; if (newTrackSelections.equals(trackSelections)) { return; diff --git a/library/src/main/java/com/google/android/exoplayer/MultiSampleSource.java b/library/src/main/java/com/google/android/exoplayer/MultiSampleSource.java index 27deef1a00..df93e5f13c 100644 --- a/library/src/main/java/com/google/android/exoplayer/MultiSampleSource.java +++ b/library/src/main/java/com/google/android/exoplayer/MultiSampleSource.java @@ -16,7 +16,6 @@ package com.google.android.exoplayer; import com.google.android.exoplayer.BufferingPolicy.LoadControl; -import com.google.android.exoplayer.util.Assertions; import android.util.Pair; @@ -28,57 +27,40 @@ import java.util.List; /** * Combines multiple {@link SampleSource} instances. */ -public final class MultiSampleSource implements SampleSource { +public final class MultiSampleSource implements SampleSource, SampleSource.Callback { private final SampleSource[] sources; private final IdentityHashMap trackStreamSources; private final int[] selectedTrackCounts; - private boolean prepared; - private boolean seenFirstTrackSelection; + private Callback callback; + private int pendingChildPrepareCount; private long durationUs; private TrackGroupArray trackGroups; + + private boolean seenFirstTrackSelection; private SampleSource[] enabledSources; public MultiSampleSource(SampleSource... sources) { this.sources = sources; + pendingChildPrepareCount = sources.length; trackStreamSources = new IdentityHashMap<>(); selectedTrackCounts = new int[sources.length]; } @Override - public boolean prepare(long positionUs, LoadControl loadControl) throws IOException { - if (prepared) { - return true; - } - boolean sourcesPrepared = true; + public void prepare(Callback callback, LoadControl loadControl, long positionUs) { + this.callback = callback; for (SampleSource source : sources) { - sourcesPrepared &= source.prepare(positionUs, loadControl); + source.prepare(this, loadControl, positionUs); } - if (!sourcesPrepared) { - return false; - } - durationUs = 0; - int totalTrackGroupCount = 0; + } + + @Override + public void maybeThrowPrepareError() throws IOException { for (SampleSource source : sources) { - totalTrackGroupCount += source.getTrackGroups().length; - if (durationUs != C.UNSET_TIME_US) { - long sourceDurationUs = source.getDurationUs(); - durationUs = sourceDurationUs == C.UNSET_TIME_US - ? C.UNSET_TIME_US : Math.max(durationUs, sourceDurationUs); - } + source.maybeThrowPrepareError(); } - TrackGroup[] trackGroupArray = new TrackGroup[totalTrackGroupCount]; - int trackGroupIndex = 0; - for (SampleSource source : sources) { - int sourceTrackGroupCount = source.getTrackGroups().length; - for (int j = 0; j < sourceTrackGroupCount; j++) { - trackGroupArray[trackGroupIndex++] = source.getTrackGroups().get(j); - } - } - trackGroups = new TrackGroupArray(trackGroupArray); - prepared = true; - return true; } @Override @@ -94,13 +76,12 @@ public final class MultiSampleSource implements SampleSource { @Override public TrackStream[] selectTracks(List oldStreams, List newSelections, long positionUs) { - Assertions.checkState(prepared); TrackStream[] newStreams = new TrackStream[newSelections.size()]; // Select tracks for each source. int enabledSourceCount = 0; for (int i = 0; i < sources.length; i++) { selectedTrackCounts[i] += selectTracks(sources[i], oldStreams, newSelections, positionUs, - newStreams); + newStreams, seenFirstTrackSelection); if (selectedTrackCounts[i] > 0) { enabledSourceCount++; } @@ -166,10 +147,40 @@ public final class MultiSampleSource implements SampleSource { } } + // SampleSource.Callback implementation + + @Override + public void onSourcePrepared(SampleSource ignored) { + if (--pendingChildPrepareCount > 0) { + return; + } + durationUs = 0; + int totalTrackGroupCount = 0; + for (SampleSource source : sources) { + totalTrackGroupCount += source.getTrackGroups().length; + if (durationUs != C.UNSET_TIME_US) { + long sourceDurationUs = source.getDurationUs(); + durationUs = sourceDurationUs == C.UNSET_TIME_US + ? C.UNSET_TIME_US : Math.max(durationUs, sourceDurationUs); + } + } + TrackGroup[] trackGroupArray = new TrackGroup[totalTrackGroupCount]; + int trackGroupIndex = 0; + for (SampleSource source : sources) { + int sourceTrackGroupCount = source.getTrackGroups().length; + for (int j = 0; j < sourceTrackGroupCount; j++) { + trackGroupArray[trackGroupIndex++] = source.getTrackGroups().get(j); + } + } + trackGroups = new TrackGroupArray(trackGroupArray); + callback.onSourcePrepared(this); + } + // Internal methods. private int selectTracks(SampleSource source, List allOldStreams, - List allNewSelections, long positionUs, TrackStream[] allNewStreams) { + List allNewSelections, long positionUs, TrackStream[] allNewStreams, + boolean seenFirstTrackSelection) { // Get the subset of the old streams for the source. ArrayList oldStreams = new ArrayList<>(); for (int i = 0; i < allOldStreams.size(); i++) { diff --git a/library/src/main/java/com/google/android/exoplayer/SampleSource.java b/library/src/main/java/com/google/android/exoplayer/SampleSource.java index 824f080220..8d84d7de74 100644 --- a/library/src/main/java/com/google/android/exoplayer/SampleSource.java +++ b/library/src/main/java/com/google/android/exoplayer/SampleSource.java @@ -26,18 +26,45 @@ import java.util.List; public interface SampleSource { /** - * Prepares the source, or does nothing if the source is already prepared. - *

- * {@link #selectTracks(List, List, long)} must be called after the source is prepared to - * make an initial track selection. This is true even if the caller does not wish to select any - * tracks. - * - * @param positionUs The player's current playback position. - * @param loadControl A {@link LoadControl} to determine when to load data. - * @return True if the source is prepared, false otherwise. - * @throws IOException If there's an error preparing the source. + * A callback to be notified of {@link SampleSource} events. */ - boolean prepare(long positionUs, LoadControl loadControl) throws IOException; + interface Callback { + + /** + * Invoked by the source when preparation completes. + *

+ * May be called from any thread. After invoking this method, the source can expect + * {@link #selectTracks(List, List, long)} to be invoked when the initial track selection. + * + * @param source The prepared source. + */ + void onSourcePrepared(SampleSource source); + + } + + /** + * Starts preparation of the source. + *

+ * {@link Callback#onSourcePrepared(SampleSource)} is invoked when preparation completes. If + * preparation fails, {@link #maybeThrowPrepareError()} will throw an {@link IOException} if + * invoked. + * + * @param callback A callback to receive updates from the source. + * @param loadControl A {@link LoadControl} to determine when to load data. + * @param positionUs The player's current playback position. + * @return True if the source is prepared, false otherwise. + */ + void prepare(Callback callback, LoadControl loadControl, long positionUs); + + /** + * Throws an error that's preventing the source from becoming prepared. Does nothing if no such + * error exists. + *

+ * This method should only be called before the source has completed preparation. + * + * @throws IOException The underlying error. + */ + void maybeThrowPrepareError() throws IOException; /** * Returns the duration of the source in microseconds, or {@link C#UNSET_TIME_US} if not known. diff --git a/library/src/main/java/com/google/android/exoplayer/SingleSampleSource.java b/library/src/main/java/com/google/android/exoplayer/SingleSampleSource.java index e08a2bb3ef..61c00ff0d3 100644 --- a/library/src/main/java/com/google/android/exoplayer/SingleSampleSource.java +++ b/library/src/main/java/com/google/android/exoplayer/SingleSampleSource.java @@ -109,9 +109,13 @@ public final class SingleSampleSource implements SampleSource, TrackStream, // SampleSource implementation. @Override - public boolean prepare(long positionUs, LoadControl loadControl) { - // TODO: Use the load control. - return true; + public void prepare(Callback callback, LoadControl loadControl, long positionUs) { + callback.onSourcePrepared(this); + } + + @Override + public void maybeThrowPrepareError() throws IOException { + // Do nothing. } @Override diff --git a/library/src/main/java/com/google/android/exoplayer/TrackRenderer.java b/library/src/main/java/com/google/android/exoplayer/TrackRenderer.java index 3de9634aa9..57fff44577 100644 --- a/library/src/main/java/com/google/android/exoplayer/TrackRenderer.java +++ b/library/src/main/java/com/google/android/exoplayer/TrackRenderer.java @@ -112,6 +112,10 @@ public abstract class TrackRenderer implements ExoPlayerComponent { private boolean readEndOfStream; private boolean streamIsFinal; + public TrackRenderer() { + readEndOfStream = true; + } + /** * Sets the index of this renderer within the player. * diff --git a/library/src/main/java/com/google/android/exoplayer/chunk/ChunkTrackStream.java b/library/src/main/java/com/google/android/exoplayer/chunk/ChunkTrackStream.java index c69c06f8e9..5becab3308 100644 --- a/library/src/main/java/com/google/android/exoplayer/chunk/ChunkTrackStream.java +++ b/library/src/main/java/com/google/android/exoplayer/chunk/ChunkTrackStream.java @@ -70,8 +70,7 @@ public class ChunkTrackStream implements TrackStream, * @param eventDispatcher A dispatcher to notify of events. */ public ChunkTrackStream(int trackType, T chunkSource, LoadControl loadControl, long positionUs, - int minLoadableRetryCount, - EventDispatcher eventDispatcher) { + int minLoadableRetryCount, EventDispatcher eventDispatcher) { this.trackType = trackType; this.chunkSource = chunkSource; this.loadControl = loadControl; diff --git a/library/src/main/java/com/google/android/exoplayer/dash/DashSampleSource.java b/library/src/main/java/com/google/android/exoplayer/dash/DashSampleSource.java index 7f88d3d061..9f8e0909ca 100644 --- a/library/src/main/java/com/google/android/exoplayer/dash/DashSampleSource.java +++ b/library/src/main/java/com/google/android/exoplayer/dash/DashSampleSource.java @@ -39,7 +39,6 @@ import com.google.android.exoplayer.upstream.BandwidthMeter; import com.google.android.exoplayer.upstream.DataSource; import com.google.android.exoplayer.upstream.DataSourceFactory; import com.google.android.exoplayer.upstream.Loader; -import com.google.android.exoplayer.upstream.Loader.Callback; import com.google.android.exoplayer.upstream.ParsingLoadable; import com.google.android.exoplayer.util.Util; @@ -84,6 +83,7 @@ public final class DashSampleSource implements SampleSource { private long manifestLoadEndTimestamp; private MediaPresentationDescription manifest; + private Callback callback; private LoadControl loadControl; private boolean prepared; private long durationUs; @@ -107,16 +107,15 @@ public final class DashSampleSource implements SampleSource { } @Override - public boolean prepare(long positionUs, LoadControl loadControl) throws IOException { - if (prepared) { - return true; - } + public void prepare(Callback callback, LoadControl loadControl, long positionUs) { + this.callback = callback; this.loadControl = loadControl; + startLoadingManifest(); + } + + @Override + public void maybeThrowPrepareError() throws IOException { loader.maybeThrowError(); - if (!loader.isLoading() && manifest == null) { - startLoadingManifest(); - } - return false; } @Override @@ -231,6 +230,7 @@ public final class DashSampleSource implements SampleSource { resolveUtcTimingElement(manifest.utcTiming); } else { prepared = true; + callback.onSourcePrepared(this); } } else { for (ChunkTrackStream trackStream : trackStreams) { @@ -308,16 +308,18 @@ public final class DashSampleSource implements SampleSource { private void onUtcTimestampResolved(long elapsedRealtimeOffsetMs) { this.elapsedRealtimeOffset = elapsedRealtimeOffsetMs; prepared = true; + callback.onSourcePrepared(this); } private void onUtcTimestampResolutionError(IOException error) { Log.e(TAG, "Failed to resolve UtcTiming element.", error); // Be optimistic and continue in the hope that the device clock is correct. prepared = true; + callback.onSourcePrepared(this); } - private void startLoading(ParsingLoadable loadable, Callback> callback, - int minRetryCount) { + private void startLoading(ParsingLoadable loadable, + Loader.Callback> callback, int minRetryCount) { long elapsedRealtimeMs = loader.startLoading(loadable, callback, minRetryCount); eventDispatcher.loadStarted(loadable.dataSpec, loadable.type, elapsedRealtimeMs); } diff --git a/library/src/main/java/com/google/android/exoplayer/extractor/DefaultTrackOutput.java b/library/src/main/java/com/google/android/exoplayer/extractor/DefaultTrackOutput.java index 5c59983953..aec311b711 100644 --- a/library/src/main/java/com/google/android/exoplayer/extractor/DefaultTrackOutput.java +++ b/library/src/main/java/com/google/android/exoplayer/extractor/DefaultTrackOutput.java @@ -38,6 +38,20 @@ import java.util.concurrent.atomic.AtomicInteger; */ public final class DefaultTrackOutput implements TrackOutput { + /** + * A listener for changes to the upstream format. + */ + public interface UpstreamFormatChangedListener { + + /** + * Invoked on the loading thread when an upstream format change occurs. + * + * @param format The new upstream format. + */ + void onUpstreamFormatChanged(Format format); + + } + private static final int INITIAL_SCRATCH_SIZE = 32; private static final int STATE_ENABLED = 0; @@ -64,6 +78,7 @@ public final class DefaultTrackOutput implements TrackOutput { private int lastAllocationOffset; private boolean needKeyframe; private boolean pendingSplice; + private UpstreamFormatChangedListener upstreamFormatChangeListener; /** * @param allocator An {@link Allocator} from which allocations for sample data can be obtained. @@ -391,6 +406,15 @@ public final class DefaultTrackOutput implements TrackOutput { // Called by the loading thread. + /** + * Sets a listener to be notified of changes to the upstream format. + * + * @param listener The listener. + */ + public void setUpstreamFormatChangeListener(UpstreamFormatChangedListener listener) { + upstreamFormatChangeListener = listener; + } + /** * Like {@link #format(Format)}, but with an offset that will be added to the timestamps of * samples subsequently queued to the buffer. The offset is also used to adjust @@ -407,7 +431,11 @@ public final class DefaultTrackOutput implements TrackOutput { @Override public void format(Format format) { - infoQueue.format(getAdjustedSampleFormat(format, sampleOffsetUs)); + Format adjustedFormat = getAdjustedSampleFormat(format, sampleOffsetUs); + infoQueue.format(adjustedFormat); + if (upstreamFormatChangeListener != null) { + upstreamFormatChangeListener.onUpstreamFormatChanged(adjustedFormat); + } } @Override diff --git a/library/src/main/java/com/google/android/exoplayer/extractor/ExtractorSampleSource.java b/library/src/main/java/com/google/android/exoplayer/extractor/ExtractorSampleSource.java index b251e90249..b44db49f1c 100644 --- a/library/src/main/java/com/google/android/exoplayer/extractor/ExtractorSampleSource.java +++ b/library/src/main/java/com/google/android/exoplayer/extractor/ExtractorSampleSource.java @@ -18,6 +18,7 @@ package com.google.android.exoplayer.extractor; import com.google.android.exoplayer.BufferingPolicy.LoadControl; import com.google.android.exoplayer.C; import com.google.android.exoplayer.DecoderInputBuffer; +import com.google.android.exoplayer.Format; import com.google.android.exoplayer.FormatHolder; import com.google.android.exoplayer.ParserException; import com.google.android.exoplayer.SampleSource; @@ -25,6 +26,7 @@ import com.google.android.exoplayer.TrackGroup; import com.google.android.exoplayer.TrackGroupArray; import com.google.android.exoplayer.TrackSelection; import com.google.android.exoplayer.TrackStream; +import com.google.android.exoplayer.extractor.DefaultTrackOutput.UpstreamFormatChangedListener; import com.google.android.exoplayer.upstream.BandwidthMeter; import com.google.android.exoplayer.upstream.DataSource; import com.google.android.exoplayer.upstream.DataSourceFactory; @@ -71,7 +73,8 @@ import java.util.List; * from {@link Extractor#sniff(ExtractorInput)} will be used. */ public final class ExtractorSampleSource implements SampleSource, ExtractorOutput, - Loader.Callback { + Loader.Callback, + UpstreamFormatChangedListener { /** * Interface definition for a callback to be notified of {@link ExtractorSampleSource} events. @@ -129,11 +132,12 @@ public final class ExtractorSampleSource implements SampleSource, ExtractorOutpu private final Loader loader; private final ExtractorHolder extractorHolder; - private volatile boolean tracksBuilt; - private volatile SeekMap seekMap; - + private Callback callback; private LoadControl loadControl; + private SeekMap seekMap; + private boolean tracksBuilt; private boolean prepared; + private boolean seenFirstTrackSelection; private boolean notifyReset; private int enabledTrackCount; @@ -304,31 +308,16 @@ public final class ExtractorSampleSource implements SampleSource, ExtractorOutpu // SampleSource implementation. @Override - public boolean prepare(long positionUs, LoadControl loadControl) throws IOException { - if (prepared) { - return true; - } + public void prepare(Callback callback, LoadControl loadControl, long positionUs) { + this.callback = callback; this.loadControl = loadControl; - if (seekMap != null && tracksBuilt && haveFormatsForAllTracks()) { - loadCondition.close(); - int trackCount = sampleQueues.length; - TrackGroup[] trackArray = new TrackGroup[trackCount]; - trackEnabledStates = new boolean[trackCount]; - durationUs = seekMap.getDurationUs(); - for (int i = 0; i < trackCount; i++) { - trackArray[i] = new TrackGroup(sampleQueues[i].getUpstreamFormat()); - } - tracks = new TrackGroupArray(trackArray); - prepared = true; - return true; - } - // We're not prepared. + loadCondition.open(); + startLoading(); + } + + @Override + public void maybeThrowPrepareError() throws IOException { maybeThrowError(); - if (!loader.isLoading()) { - loadCondition.open(); - startLoading(); - } - return false; } @Override @@ -521,6 +510,7 @@ public final class ExtractorSampleSource implements SampleSource, ExtractorOutpu public TrackOutput track(int id) { sampleQueues = Arrays.copyOf(sampleQueues, sampleQueues.length + 1); DefaultTrackOutput sampleQueue = new DefaultTrackOutput(loadControl.getAllocator()); + sampleQueue.setUpstreamFormatChangeListener(this); sampleQueues[sampleQueues.length - 1] = sampleQueue; return sampleQueue; } @@ -528,15 +518,46 @@ public final class ExtractorSampleSource implements SampleSource, ExtractorOutpu @Override public void endTracks() { tracksBuilt = true; + maybeFinishPrepare(); } @Override public void seekMap(SeekMap seekMap) { this.seekMap = seekMap; + maybeFinishPrepare(); + } + + // UpstreamFormatChangedListener implementation + + @Override + public void onUpstreamFormatChanged(Format format) { + maybeFinishPrepare(); } // Internal methods. + private void maybeFinishPrepare() { + if (seekMap == null || !tracksBuilt) { + return; + } + for (DefaultTrackOutput sampleQueue : sampleQueues) { + if (sampleQueue.getUpstreamFormat() == null) { + return; + } + } + loadCondition.close(); + int trackCount = sampleQueues.length; + TrackGroup[] trackArray = new TrackGroup[trackCount]; + trackEnabledStates = new boolean[trackCount]; + durationUs = seekMap.getDurationUs(); + for (int i = 0; i < trackCount; i++) { + trackArray[i] = new TrackGroup(sampleQueues[i].getUpstreamFormat()); + } + tracks = new TrackGroupArray(trackArray); + prepared = true; + callback.onSourcePrepared(this); + } + private void copyLengthFromLoader(ExtractingLoadable loadable) { if (length == C.LENGTH_UNBOUNDED) { length = loadable.length; @@ -618,15 +639,6 @@ public final class ExtractorSampleSource implements SampleSource, ExtractorOutpu return largestQueuedTimestampUs; } - private boolean haveFormatsForAllTracks() { - for (DefaultTrackOutput sampleQueue : sampleQueues) { - if (sampleQueue.getUpstreamFormat() == null) { - return false; - } - } - return true; - } - private boolean isPendingReset() { return pendingResetPositionUs != C.UNSET_TIME_US; } 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 447ace0ef3..e7ac1f062b 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 @@ -53,7 +53,7 @@ import java.util.List; * A {@link SampleSource} for HLS streams. */ public final class HlsSampleSource implements SampleSource, - Loader.Callback> { + Loader.Callback>, HlsTrackStreamWrapper.Callback { /** * The minimum number of times to retry loading data prior to failing. @@ -70,7 +70,11 @@ public final class HlsSampleSource implements SampleSource, private final DataSource manifestDataSource; private final HlsPlaylistParser manifestParser; + private Callback callback; private LoadControl loadControl; + private long preparePositionUs; + private int pendingPrepareCount; + private boolean seenFirstTrackSelection; private long durationUs; private boolean isLive; @@ -96,50 +100,26 @@ public final class HlsSampleSource implements SampleSource, } @Override - public boolean prepare(long positionUs, LoadControl loadControl) throws IOException { - if (trackGroups != null) { - return true; - } - + public void prepare(Callback callback, LoadControl loadControl, long positionUs) { + this.callback = callback; this.loadControl = loadControl; + this.preparePositionUs = positionUs; + ParsingLoadable loadable = new ParsingLoadable<>(manifestDataSource, + manifestUri, C.DATA_TYPE_MANIFEST, manifestParser); + long elapsedRealtimeMs = manifestFetcher.startLoading(loadable, this, + MIN_LOADABLE_RETRY_COUNT); + eventDispatcher.loadStarted(loadable.dataSpec, loadable.type, elapsedRealtimeMs); + } + + @Override + public void maybeThrowPrepareError() throws IOException { if (trackStreamWrappers == null) { manifestFetcher.maybeThrowError(); - if (!manifestFetcher.isLoading()) { - ParsingLoadable loadable = new ParsingLoadable<>(manifestDataSource, - manifestUri, C.DATA_TYPE_MANIFEST, manifestParser); - long elapsedRealtimeMs = manifestFetcher.startLoading(loadable, this, - MIN_LOADABLE_RETRY_COUNT); - eventDispatcher.loadStarted(loadable.dataSpec, loadable.type, elapsedRealtimeMs); - } - return false; - } - - boolean trackStreamWrappersPrepared = true; - for (HlsTrackStreamWrapper trackStreamWrapper : trackStreamWrappers) { - trackStreamWrappersPrepared &= trackStreamWrapper.prepare(positionUs); - } - if (!trackStreamWrappersPrepared) { - return false; - } - - // The wrapper at index 0 is the one of type TRACK_TYPE_DEFAULT. - durationUs = trackStreamWrappers[0].getDurationUs(); - isLive = trackStreamWrappers[0].isLive(); - - int totalTrackGroupCount = 0; - for (HlsTrackStreamWrapper trackStreamWrapper : trackStreamWrappers) { - totalTrackGroupCount += trackStreamWrapper.getTrackGroups().length; - } - TrackGroup[] trackGroupArray = new TrackGroup[totalTrackGroupCount]; - int trackGroupIndex = 0; - for (HlsTrackStreamWrapper trackStreamWrapper : trackStreamWrappers) { - int wrapperTrackGroupCount = trackStreamWrapper.getTrackGroups().length; - for (int j = 0; j < wrapperTrackGroupCount; j++) { - trackGroupArray[trackGroupIndex++] = trackStreamWrapper.getTrackGroups().get(j); + } else { + for (HlsTrackStreamWrapper trackStreamWrapper : trackStreamWrappers) { + trackStreamWrapper.maybeThrowPrepareError(); } } - trackGroups = new TrackGroupArray(trackGroupArray); - return true; } @Override @@ -237,6 +217,10 @@ public final class HlsSampleSource implements SampleSource, trackStreamWrappers = new HlsTrackStreamWrapper[trackStreamWrapperList.size()]; trackStreamWrapperList.toArray(trackStreamWrappers); selectedTrackCounts = new int[trackStreamWrappers.length]; + pendingPrepareCount = trackStreamWrappers.length; + for (HlsTrackStreamWrapper trackStreamWrapper : trackStreamWrappers) { + trackStreamWrapper.prepare(); + } } @Override @@ -255,6 +239,34 @@ public final class HlsSampleSource implements SampleSource, return isFatal ? Loader.DONT_RETRY_FATAL : Loader.RETRY; } + // HlsTrackStreamWrapper callback. + + @Override + public void onPrepared() { + if (--pendingPrepareCount > 0) { + return; + } + + // The wrapper at index 0 is the one of type TRACK_TYPE_DEFAULT. + durationUs = trackStreamWrappers[0].getDurationUs(); + isLive = trackStreamWrappers[0].isLive(); + + int totalTrackGroupCount = 0; + for (HlsTrackStreamWrapper trackStreamWrapper : trackStreamWrappers) { + totalTrackGroupCount += trackStreamWrapper.getTrackGroups().length; + } + TrackGroup[] trackGroupArray = new TrackGroup[totalTrackGroupCount]; + int trackGroupIndex = 0; + for (HlsTrackStreamWrapper trackStreamWrapper : trackStreamWrappers) { + int wrapperTrackGroupCount = trackStreamWrapper.getTrackGroups().length; + for (int j = 0; j < wrapperTrackGroupCount; j++) { + trackGroupArray[trackGroupIndex++] = trackStreamWrapper.getTrackGroups().get(j); + } + } + trackGroups = new TrackGroupArray(trackGroupArray); + callback.onSourcePrepared(this); + } + // Internal methods. private List buildTrackStreamWrappers(HlsPlaylist playlist) { @@ -296,16 +308,18 @@ public final class HlsSampleSource implements SampleSource, } else { // Leave the enabled variants unchanged. They're likely either all video or all audio. } - Variant[] variants = new Variant[selectedVariants.size()]; - selectedVariants.toArray(variants); - trackStreamWrappers.add(buildTrackStreamWrapper(C.TRACK_TYPE_DEFAULT, baseUri, variants, - new FormatEvaluator.AdaptiveEvaluator(bandwidthMeter), masterPlaylist.muxedAudioFormat, - masterPlaylist.muxedCaptionFormat)); + if (!selectedVariants.isEmpty()) { + Variant[] variants = new Variant[selectedVariants.size()]; + selectedVariants.toArray(variants); + trackStreamWrappers.add(buildTrackStreamWrapper(C.TRACK_TYPE_DEFAULT, baseUri, variants, + new FormatEvaluator.AdaptiveEvaluator(bandwidthMeter), masterPlaylist.muxedAudioFormat, + masterPlaylist.muxedCaptionFormat)); + } // Build the audio stream wrapper if applicable. List audioVariants = masterPlaylist.audios; if (!audioVariants.isEmpty()) { - variants = new Variant[audioVariants.size()]; + Variant[] variants = new Variant[audioVariants.size()]; audioVariants.toArray(variants); trackStreamWrappers.add(buildTrackStreamWrapper(C.TRACK_TYPE_AUDIO, baseUri, variants, null, null, null)); @@ -314,7 +328,7 @@ public final class HlsSampleSource implements SampleSource, // Build the text stream wrapper if applicable. List subtitleVariants = masterPlaylist.subtitles; if (!subtitleVariants.isEmpty()) { - variants = new Variant[subtitleVariants.size()]; + Variant[] variants = new Variant[subtitleVariants.size()]; subtitleVariants.toArray(variants); trackStreamWrappers.add(buildTrackStreamWrapper(C.TRACK_TYPE_TEXT, baseUri, variants, null, null, null)); @@ -329,8 +343,9 @@ public final class HlsSampleSource implements SampleSource, DataSource dataSource = dataSourceFactory.createDataSource(bandwidthMeter); HlsChunkSource defaultChunkSource = new HlsChunkSource(baseUri, variants, dataSource, timestampAdjusterProvider, formatEvaluator); - return new HlsTrackStreamWrapper(trackType, defaultChunkSource, loadControl, muxedAudioFormat, - muxedCaptionFormat, MIN_LOADABLE_RETRY_COUNT, eventDispatcher); + return new HlsTrackStreamWrapper(trackType, this, defaultChunkSource, loadControl, + preparePositionUs, muxedAudioFormat, muxedCaptionFormat, MIN_LOADABLE_RETRY_COUNT, + eventDispatcher); } private int selectTracks(HlsTrackStreamWrapper trackStreamWrapper, diff --git a/library/src/main/java/com/google/android/exoplayer/hls/HlsTrackStreamWrapper.java b/library/src/main/java/com/google/android/exoplayer/hls/HlsTrackStreamWrapper.java index eb819dd68f..9751918934 100644 --- a/library/src/main/java/com/google/android/exoplayer/hls/HlsTrackStreamWrapper.java +++ b/library/src/main/java/com/google/android/exoplayer/hls/HlsTrackStreamWrapper.java @@ -28,6 +28,7 @@ import com.google.android.exoplayer.TrackStream; import com.google.android.exoplayer.chunk.Chunk; import com.google.android.exoplayer.chunk.ChunkHolder; import com.google.android.exoplayer.extractor.DefaultTrackOutput; +import com.google.android.exoplayer.extractor.DefaultTrackOutput.UpstreamFormatChangedListener; import com.google.android.exoplayer.extractor.ExtractorOutput; import com.google.android.exoplayer.extractor.SeekMap; import com.google.android.exoplayer.upstream.Loader; @@ -44,7 +45,20 @@ import java.util.List; * Loads {@link HlsMediaChunk}s obtained from a {@link HlsChunkSource}, and provides * {@link TrackStream}s from which the loaded media can be consumed. */ -/* package */ final class HlsTrackStreamWrapper implements Loader.Callback, ExtractorOutput { +/* package */ final class HlsTrackStreamWrapper implements Loader.Callback, ExtractorOutput, + UpstreamFormatChangedListener { + + /** + * A callback to be notified of events. + */ + public interface Callback { + + /** + * Invoked when the wrapper has been prepared. + */ + void onPrepared(); + + } private static final int PRIMARY_TYPE_NONE = 0; private static final int PRIMARY_TYPE_TEXT = 1; @@ -52,6 +66,7 @@ import java.util.List; private static final int PRIMARY_TYPE_VIDEO = 3; private final int trackType; + private final Callback callback; private final HlsChunkSource chunkSource; private final LoadControl loadControl; private final Format muxedAudioFormat; @@ -85,8 +100,10 @@ import java.util.List; /** * @param trackType The type of the track. One of the {@link C} {@code TRACK_TYPE_*} constants. + * @param callback A callback for the wrapper. * @param chunkSource A {@link HlsChunkSource} from which chunks to load are obtained. * @param loadControl Controls when the source is permitted to load data. + * @param positionUs The position from which to start loading media. * @param muxedAudioFormat If HLS master playlist indicates that the stream contains muxed audio, * this is the audio {@link Format} as defined by the playlist. * @param muxedCaptionFormat If HLS master playlist indicates that the stream contains muxed @@ -95,10 +112,11 @@ import java.util.List; * before propagating an error. * @param eventDispatcher A dispatcher to notify of events. */ - public HlsTrackStreamWrapper(int trackType, HlsChunkSource chunkSource, LoadControl loadControl, - Format muxedAudioFormat, Format muxedCaptionFormat, int minLoadableRetryCount, - EventDispatcher eventDispatcher) { + public HlsTrackStreamWrapper(int trackType, Callback callback, HlsChunkSource chunkSource, + LoadControl loadControl, long positionUs, Format muxedAudioFormat, Format muxedCaptionFormat, + int minLoadableRetryCount, EventDispatcher eventDispatcher) { this.trackType = trackType; + this.callback = callback; this.chunkSource = chunkSource; this.loadControl = loadControl; this.muxedAudioFormat = muxedAudioFormat; @@ -110,44 +128,16 @@ import java.util.List; sampleQueues = new SparseArray<>(); mediaChunks = new LinkedList<>(); readingEnabled = true; - pendingResetPositionUs = C.UNSET_TIME_US; + pendingResetPositionUs = positionUs; + downstreamPositionUs = positionUs; } - public boolean prepare(long positionUs) throws IOException { - if (prepared) { - return true; - } - if (chunkSource.getTrackCount() == 0) { - trackGroups = new TrackGroupArray(); - prepared = true; - return true; - } - if (sampleQueuesBuilt) { - boolean canBuildTracks = true; - int sampleQueueCount = sampleQueues.size(); - for (int i = 0; i < sampleQueueCount; i++) { - if (sampleQueues.valueAt(i).getUpstreamFormat() == null) { - canBuildTracks = false; - break; - } - } - if (canBuildTracks) { - buildTracks(); - prepared = true; - return true; - } - } - // We're not prepared. + public void prepare() { + maybeStartLoading(); + } + + public void maybeThrowPrepareError() throws IOException { maybeThrowError(); - 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; } public long getDurationUs() { @@ -376,6 +366,7 @@ import java.util.List; return sampleQueues.get(id); } DefaultTrackOutput trackOutput = new DefaultTrackOutput(loadControl.getAllocator()); + trackOutput.setUpstreamFormatChangeListener(this); sampleQueues.put(id, trackOutput); return trackOutput; } @@ -383,6 +374,7 @@ import java.util.List; @Override public void endTracks() { sampleQueuesBuilt = true; + maybeFinishPrepare(); } @Override @@ -390,8 +382,30 @@ import java.util.List; // Do nothing. } + // UpstreamFormatChangedListener implementation. + + @Override + public void onUpstreamFormatChanged(Format format) { + maybeFinishPrepare(); + } + // Internal methods. + private void maybeFinishPrepare() { + if (!sampleQueuesBuilt) { + return; + } + int sampleQueueCount = sampleQueues.size(); + for (int i = 0; i < sampleQueueCount; i++) { + if (sampleQueues.valueAt(i).getUpstreamFormat() == null) { + return; + } + } + buildTracks(); + prepared = true; + callback.onPrepared(); + } + /** * Builds tracks that are exposed by this {@link HlsTrackStreamWrapper} instance, as well as * internal data-structures required for operation. diff --git a/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingSampleSource.java b/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingSampleSource.java index d458287db3..e0e7f59d3d 100644 --- a/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingSampleSource.java +++ b/library/src/main/java/com/google/android/exoplayer/smoothstreaming/SmoothStreamingSampleSource.java @@ -73,6 +73,7 @@ public final class SmoothStreamingSampleSource implements SampleSource, private long manifestLoadStartTimestamp; private SmoothStreamingManifest manifest; + private Callback callback; private LoadControl loadControl; private boolean prepared; private long durationUs; @@ -97,16 +98,15 @@ public final class SmoothStreamingSampleSource implements SampleSource, } @Override - public boolean prepare(long positionUs, LoadControl loadControl) throws IOException { - if (prepared) { - return true; - } + public void prepare(Callback callback, LoadControl loadControl, long positionUs) { + this.callback = callback; this.loadControl = loadControl; + startLoadingManifest(); + } + + @Override + public void maybeThrowPrepareError() throws IOException { manifestLoader.maybeThrowError(); - if (!manifestLoader.isLoading()) { - startLoadingManifest(); - } - return false; } @Override @@ -218,6 +218,7 @@ public final class SmoothStreamingSampleSource implements SampleSource, new TrackEncryptionBox(true, INITIALIZATION_VECTOR_SIZE, keyId)}; } prepared = true; + callback.onSourcePrepared(this); } else { for (ChunkTrackStream trackStream : trackStreams) { trackStream.getChunkSource().updateManifest(manifest); diff --git a/library/src/main/java/com/google/android/exoplayer/upstream/Loader.java b/library/src/main/java/com/google/android/exoplayer/upstream/Loader.java index 2c3cb88ee1..3599fb6b88 100644 --- a/library/src/main/java/com/google/android/exoplayer/upstream/Loader.java +++ b/library/src/main/java/com/google/android/exoplayer/upstream/Loader.java @@ -46,7 +46,7 @@ public final class Loader { } /** - * Interface definition of an object that can be loaded using a {@link Loader}. + * An object that can be loaded using a {@link Loader}. */ public interface Loadable { @@ -73,7 +73,7 @@ public final class Loader { } /** - * Interface definition for a callback to be notified of {@link Loader} events. + * A callback to be notified of {@link Loader} events. */ public interface Callback {