diff --git a/RELEASENOTES.md b/RELEASENOTES.md index d7bd90055e..ec11b5b7ed 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -9,6 +9,9 @@ ([#3314](https://github.com/google/ExoPlayer/issues/3314)). * Do not retry failed loads whose error is `FileNotFoundException`. * Prevent Cea608Decoder from generating Subtitles with null Cues list +* Caching: Cache data with unknown length by default. The previous flag to opt in + to this behavior (`DataSpec.FLAG_ALLOW_CACHING_UNKNOWN_LENGTH`) has been + replaced with an opt out flag (`DataSpec.FLAG_DONT_CACHE_IF_LENGTH_UNKNOWN`). ### 2.9.2 ### diff --git a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsMediaSource.java b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsMediaSource.java index 400061d019..85042c4354 100644 --- a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsMediaSource.java +++ b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsMediaSource.java @@ -76,6 +76,12 @@ public final class ImaAdsMediaSource extends BaseMediaSource implements SourceIn adUiViewGroup, eventHandler, eventListener); } + @Override + @Nullable + public Object getTag() { + return adsMediaSource.getTag(); + } + @Override public void prepareSourceInternal( final ExoPlayer player, diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioRendererEventListener.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioRendererEventListener.java index 7c3c1481fc..eff7bc8de2 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioRendererEventListener.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioRendererEventListener.java @@ -25,7 +25,8 @@ import com.google.android.exoplayer2.decoder.DecoderCounters; import com.google.android.exoplayer2.util.Assertions; /** - * Listener of audio {@link Renderer} events. + * Listener of audio {@link Renderer} events. All methods have no-op default implementations to + * allow selective overrides. */ public interface AudioRendererEventListener { @@ -35,14 +36,14 @@ public interface AudioRendererEventListener { * @param counters {@link DecoderCounters} that will be updated by the renderer for as long as it * remains enabled. */ - void onAudioEnabled(DecoderCounters counters); + default void onAudioEnabled(DecoderCounters counters) {} /** * Called when the audio session is set. * * @param audioSessionId The audio session id. */ - void onAudioSessionId(int audioSessionId); + default void onAudioSessionId(int audioSessionId) {} /** * Called when a decoder is created. @@ -52,15 +53,15 @@ public interface AudioRendererEventListener { * finished. * @param initializationDurationMs The time taken to initialize the decoder in milliseconds. */ - void onAudioDecoderInitialized(String decoderName, long initializedTimestampMs, - long initializationDurationMs); + default void onAudioDecoderInitialized( + String decoderName, long initializedTimestampMs, long initializationDurationMs) {} /** * Called when the format of the media being consumed by the renderer changes. * * @param format The new format. */ - void onAudioInputFormatChanged(Format format); + default void onAudioInputFormatChanged(Format format) {} /** * Called when an {@link AudioSink} underrun occurs. @@ -71,14 +72,15 @@ public interface AudioRendererEventListener { * as the buffered media can have a variable bitrate so the duration may be unknown. * @param elapsedSinceLastFeedMs The time since the {@link AudioSink} was last fed data. */ - void onAudioSinkUnderrun(int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs); + default void onAudioSinkUnderrun( + int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs) {} /** * Called when the renderer is disabled. * * @param counters {@link DecoderCounters} that were updated by the renderer. */ - void onAudioDisabled(DecoderCounters counters); + default void onAudioDisabled(DecoderCounters counters) {} /** * Dispatches events to a {@link AudioRendererEventListener}. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadHelper.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadHelper.java index 905619c6f0..044bd8cc8a 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadHelper.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadHelper.java @@ -15,15 +15,21 @@ */ package com.google.android.exoplayer2.offline; +import android.net.Uri; import android.os.Handler; import android.os.Looper; import android.support.annotation.Nullable; import com.google.android.exoplayer2.source.TrackGroupArray; +import com.google.android.exoplayer2.util.Assertions; import java.io.IOException; import java.util.List; -/** A helper for initializing and removing downloads. */ -public abstract class DownloadHelper { +/** + * A helper for initializing and removing downloads. + * + * @param The manifest type. + */ +public abstract class DownloadHelper { /** A callback to be notified when the {@link DownloadHelper} is prepared. */ public interface Callback { @@ -44,6 +50,26 @@ public abstract class DownloadHelper { void onPrepareError(DownloadHelper helper, IOException e); } + private final String downloadType; + private final Uri uri; + @Nullable private final String cacheKey; + + @Nullable private T manifest; + @Nullable private TrackGroupArray[] trackGroupArrays; + + /** + * Create download helper. + * + * @param downloadType A download type. This value will be used as {@link DownloadAction#type}. + * @param uri A {@link Uri}. + * @param cacheKey An optional cache key. + */ + public DownloadHelper(String downloadType, Uri uri, @Nullable String cacheKey) { + this.downloadType = downloadType; + this.uri = uri; + this.cacheKey = cacheKey; + } + /** * Initializes the helper for starting a download. * @@ -51,14 +77,15 @@ public abstract class DownloadHelper { * will be invoked on the calling thread unless that thread does not have an associated {@link * Looper}, in which case it will be called on the application's main thread. */ - public void prepare(final Callback callback) { + public final void prepare(final Callback callback) { final Handler handler = new Handler(Looper.myLooper() != null ? Looper.myLooper() : Looper.getMainLooper()); new Thread() { @Override public void run() { try { - prepareInternal(); + manifest = loadManifest(uri); + trackGroupArrays = getTrackGroupArrays(manifest); handler.post(() -> callback.onPrepared(DownloadHelper.this)); } catch (final IOException e) { handler.post(() -> callback.onPrepareError(DownloadHelper.this, e)); @@ -67,18 +94,20 @@ public abstract class DownloadHelper { }.start(); } - /** - * Called on a background thread during preparation. - * - * @throws IOException If preparation fails. - */ - protected abstract void prepareInternal() throws IOException; + /** Returns the manifest. Must not be called until after preparation completes. */ + public final T getManifest() { + Assertions.checkNotNull(manifest); + return manifest; + } /** * Returns the number of periods for which media is available. Must not be called until after * preparation completes. */ - public abstract int getPeriodCount(); + public final int getPeriodCount() { + Assertions.checkNotNull(trackGroupArrays); + return trackGroupArrays.length; + } /** * Returns the track groups for the given period. Must not be called until after preparation @@ -88,7 +117,10 @@ public abstract class DownloadHelper { * @return The track groups for the period. May be {@link TrackGroupArray#EMPTY} for single stream * content. */ - public abstract TrackGroupArray getTrackGroups(int periodIndex); + public final TrackGroupArray getTrackGroups(int periodIndex) { + Assertions.checkNotNull(trackGroupArrays); + return trackGroupArrays[periodIndex]; + } /** * Builds a {@link DownloadAction} for downloading the specified tracks. Must not be called until @@ -98,12 +130,41 @@ public abstract class DownloadHelper { * @param trackKeys The selected tracks. If empty, all streams will be downloaded. * @return The built {@link DownloadAction}. */ - public abstract DownloadAction getDownloadAction(@Nullable byte[] data, List trackKeys); + public final DownloadAction getDownloadAction(@Nullable byte[] data, List trackKeys) { + return DownloadAction.createDownloadAction( + downloadType, uri, toStreamKeys(trackKeys), cacheKey, data); + } /** * Builds a {@link DownloadAction} for removing the media. May be called in any state. * * @return The built {@link DownloadAction}. */ - public abstract DownloadAction getRemoveAction(); + public final DownloadAction getRemoveAction() { + return DownloadAction.createRemoveAction(downloadType, uri, cacheKey); + } + + /** + * Loads the manifest. This method is called on a background thread. + * + * @param uri The manifest uri. + * @throws IOException If loading fails. + */ + protected abstract T loadManifest(Uri uri) throws IOException; + + /** + * Returns the track group arrays for each period in the manifest. + * + * @param manifest The manifest. + * @return An array of {@link TrackGroupArray}s. One for each period in the manifest. + */ + protected abstract TrackGroupArray[] getTrackGroupArrays(T manifest); + + /** + * Converts a list of {@link TrackKey track keys} to {@link StreamKey stream keys}. + * + * @param trackKeys A list of track keys. + * @return A corresponding list of stream keys. + */ + protected abstract List toStreamKeys(List trackKeys); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadManager.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadManager.java index c5645bedf0..4a76c80d64 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadManager.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadManager.java @@ -565,7 +565,7 @@ public final class DownloadManager { */ @TargetState private volatile int targetState; - @MonotonicNonNull private volatile Downloader downloader; + @MonotonicNonNull private Downloader downloader; @MonotonicNonNull private Thread thread; @MonotonicNonNull private Throwable error; @@ -624,6 +624,7 @@ public final class DownloadManager { state = STATE_STARTED; targetState = STATE_COMPLETED; downloadManager.onTaskStateChange(this); + downloader = downloaderFactory.createDownloader(action); thread = new Thread(this); thread.start(); } @@ -648,11 +649,7 @@ public final class DownloadManager { private void stopDownloadThread(@TargetState int targetState) { this.targetState = targetState; - // TODO: The possibility of downloader being null here may prevent the download thread from - // stopping in a timely way. Fix this. - if (downloader != null) { - downloader.cancel(); - } + Assertions.checkNotNull(downloader).cancel(); Assertions.checkNotNull(thread).interrupt(); } @@ -675,7 +672,6 @@ public final class DownloadManager { logd("Task is started", this); Throwable error = null; try { - downloader = downloaderFactory.createDownloader(action); if (action.isRemoveAction) { downloader.remove(); } else { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/ProgressiveDownloadHelper.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/ProgressiveDownloadHelper.java index 6c1ceafd93..70587694c4 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/offline/ProgressiveDownloadHelper.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/ProgressiveDownloadHelper.java @@ -22,47 +22,28 @@ import java.util.Collections; import java.util.List; /** A {@link DownloadHelper} for progressive streams. */ -public final class ProgressiveDownloadHelper extends DownloadHelper { - - private final Uri uri; - private final @Nullable String customCacheKey; +public final class ProgressiveDownloadHelper extends DownloadHelper { public ProgressiveDownloadHelper(Uri uri) { this(uri, null); } public ProgressiveDownloadHelper(Uri uri, @Nullable String customCacheKey) { - this.uri = uri; - this.customCacheKey = customCacheKey; + super(DownloadAction.TYPE_PROGRESSIVE, uri, customCacheKey); } @Override - protected void prepareInternal() { - // Do nothing. + protected Void loadManifest(Uri uri) { + return null; } @Override - public int getPeriodCount() { - return 1; + protected TrackGroupArray[] getTrackGroupArrays(Void manifest) { + return new TrackGroupArray[] {TrackGroupArray.EMPTY}; } @Override - public TrackGroupArray getTrackGroups(int periodIndex) { - return TrackGroupArray.EMPTY; - } - - @Override - public DownloadAction getDownloadAction(@Nullable byte[] data, List trackKeys) { - return DownloadAction.createDownloadAction( - DownloadAction.TYPE_PROGRESSIVE, - uri, - /* keys= */ Collections.emptyList(), - customCacheKey, - data); - } - - @Override - public DownloadAction getRemoveAction() { - return DownloadAction.createRemoveAction(DownloadAction.TYPE_PROGRESSIVE, uri, customCacheKey); + protected List toStreamKeys(List trackKeys) { + return Collections.emptyList(); } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/SegmentDownloader.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/SegmentDownloader.java index e55d2a1baf..1b32abff60 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/offline/SegmentDownloader.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/SegmentDownloader.java @@ -62,7 +62,7 @@ public abstract class SegmentDownloader> impleme private static final int BUFFER_SIZE_BYTES = 128 * 1024; - private final Uri manifestUri; + private final DataSpec manifestDataSpec; private final Cache cache; private final CacheDataSource dataSource; private final CacheDataSource offlineDataSource; @@ -84,7 +84,7 @@ public abstract class SegmentDownloader> impleme */ public SegmentDownloader( Uri manifestUri, List streamKeys, DownloaderConstructorHelper constructorHelper) { - this.manifestUri = manifestUri; + this.manifestDataSpec = getCompressibleDataSpec(manifestUri); this.streamKeys = new ArrayList<>(streamKeys); this.cache = constructorHelper.getCache(); this.dataSource = constructorHelper.createCacheDataSource(); @@ -171,7 +171,7 @@ public abstract class SegmentDownloader> impleme @Override public final void remove() throws InterruptedException { try { - M manifest = getManifest(offlineDataSource, manifestUri); + M manifest = getManifest(offlineDataSource, manifestDataSpec); List segments = getSegments(offlineDataSource, manifest, true); for (int i = 0; i < segments.size(); i++) { removeDataSpec(segments.get(i).dataSpec); @@ -180,7 +180,7 @@ public abstract class SegmentDownloader> impleme // Ignore exceptions when removing. } finally { // Always attempt to remove the manifest. - removeDataSpec(new DataSpec(manifestUri)); + removeDataSpec(manifestDataSpec); } } @@ -190,11 +190,11 @@ public abstract class SegmentDownloader> impleme * Loads and parses the manifest. * * @param dataSource The {@link DataSource} through which to load. - * @param uri The manifest uri. + * @param dataSpec The manifest {@link DataSpec}. * @return The manifest. * @throws IOException If an error occurs reading data. */ - protected abstract M getManifest(DataSource dataSource, Uri uri) throws IOException; + protected abstract M getManifest(DataSource dataSource, DataSpec dataSpec) throws IOException; /** * Returns a list of all downloadable {@link Segment}s for a given manifest. @@ -217,7 +217,7 @@ public abstract class SegmentDownloader> impleme // Writes to downloadedSegments and downloadedBytes are safe. See the comment on download(). @SuppressWarnings("NonAtomicVolatileUpdate") private List initDownload() throws IOException, InterruptedException { - M manifest = getManifest(dataSource, manifestUri); + M manifest = getManifest(dataSource, manifestDataSpec); if (!streamKeys.isEmpty()) { manifest = manifest.copy(streamKeys); } @@ -252,4 +252,12 @@ public abstract class SegmentDownloader> impleme CacheUtil.remove(dataSpec, cache, cacheKeyFactory); } + protected static DataSpec getCompressibleDataSpec(Uri uri) { + return new DataSpec( + uri, + /* absoluteStreamPosition= */ 0, + /* length= */ C.LENGTH_UNSET, + /* key= */ null, + /* flags= */ DataSpec.FLAG_ALLOW_GZIP); + } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaSource.java index 3916d41b61..1dbb41dfb0 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ClippingMediaSource.java @@ -186,6 +186,12 @@ public final class ClippingMediaSource extends CompositeMediaSource { window = new Timeline.Window(); } + @Override + @Nullable + public Object getTag() { + return mediaSource.getTag(); + } + @Override public void prepareSourceInternal( ExoPlayer player, diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ConcatenatingMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ConcatenatingMediaSource.java index 1f3b01182a..26667e641f 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ConcatenatingMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ConcatenatingMediaSource.java @@ -453,6 +453,12 @@ public class ConcatenatingMediaSource extends CompositeMediaSource { mediaPeriodToChildMediaPeriodId = new HashMap<>(); } + @Override + @Nullable + public Object getTag() { + return childSource.getTag(); + } + @Override public void prepareSourceInternal( ExoPlayer player, diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSource.java index d8335131f9..21f4f20922 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSource.java @@ -219,6 +219,12 @@ public interface MediaSource { */ void removeEventListener(MediaSourceEventListener eventListener); + /** Returns the tag set on the media source, or null if none was set. */ + @Nullable + default Object getTag() { + return null; + } + /** * Starts source preparation if not yet started, and adds a listener for timeline and/or manifest * updates. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/MergingMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/MergingMediaSource.java index ecb4b10c6a..573e97cb13 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/MergingMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/MergingMediaSource.java @@ -98,6 +98,12 @@ public final class MergingMediaSource extends CompositeMediaSource { timelines = new Timeline[mediaSources.length]; } + @Override + @Nullable + public Object getTag() { + return mediaSources.length > 0 ? mediaSources[0].getTag() : null; + } + @Override public void prepareSourceInternal( ExoPlayer player, diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaSource.java index 1ac6207454..966938262e 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/SingleSampleMediaSource.java @@ -185,6 +185,7 @@ public final class SingleSampleMediaSource extends BaseMediaSource { private final LoadErrorHandlingPolicy loadErrorHandlingPolicy; private final boolean treatLoadErrorsAsEndOfStream; private final Timeline timeline; + @Nullable private final Object tag; private @Nullable TransferListener transferListener; @@ -287,14 +288,20 @@ public final class SingleSampleMediaSource extends BaseMediaSource { this.durationUs = durationUs; this.loadErrorHandlingPolicy = loadErrorHandlingPolicy; this.treatLoadErrorsAsEndOfStream = treatLoadErrorsAsEndOfStream; - dataSpec = - new DataSpec(uri, DataSpec.FLAG_ALLOW_GZIP | DataSpec.FLAG_ALLOW_CACHING_UNKNOWN_LENGTH); + this.tag = tag; + dataSpec = new DataSpec(uri, DataSpec.FLAG_ALLOW_GZIP); timeline = new SinglePeriodTimeline(durationUs, /* isSeekable= */ true, /* isDynamic= */ false, tag); } // MediaSource implementation. + @Override + @Nullable + public Object getTag() { + return tag; + } + @Override public void prepareSourceInternal( ExoPlayer player, diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsMediaSource.java index 7fc0f22bf3..19ddbd2c54 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ads/AdsMediaSource.java @@ -319,6 +319,12 @@ public final class AdsMediaSource extends CompositeMediaSource { adsLoader.setSupportedContentTypes(adMediaSourceFactory.getSupportedTypes()); } + @Override + @Nullable + public Object getTag() { + return contentMediaSource.getTag(); + } + @Override public void prepareSourceInternal( final ExoPlayer player, diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/cea/Cea608Decoder.java b/library/core/src/main/java/com/google/android/exoplayer2/text/cea/Cea608Decoder.java index 613393ed06..165b45451b 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/cea/Cea608Decoder.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/cea/Cea608Decoder.java @@ -560,6 +560,14 @@ public final class Cea608Decoder extends CeaDecoder { int oldCaptionMode = this.captionMode; this.captionMode = captionMode; + if (captionMode == CC_MODE_PAINT_ON) { + // Switching to paint-on mode should have no effect except to select the mode. + for (int i = 0; i < cueBuilders.size(); i++) { + cueBuilders.get(i).setCaptionMode(captionMode); + } + return; + } + // Clear the working memory. resetCueBuilders(); if (oldCaptionMode == CC_MODE_PAINT_ON || captionMode == CC_MODE_ROLL_UP @@ -664,6 +672,10 @@ public final class Cea608Decoder extends CeaDecoder { tabOffset = 0; } + public void setCaptionMode(int captionMode) { + this.captionMode = captionMode; + } + public void setCaptionRowCount(int captionRowCount) { this.captionRowCount = captionRowCount; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DataSpec.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DataSpec.java index 4a4cc021f4..c33c7c823f 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DataSpec.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DataSpec.java @@ -32,32 +32,29 @@ public final class DataSpec { /** * The flags that apply to any request for data. Possible flag values are {@link #FLAG_ALLOW_GZIP} - * and {@link #FLAG_ALLOW_CACHING_UNKNOWN_LENGTH}. + * and {@link #FLAG_DONT_CACHE_IF_LENGTH_UNKNOWN}. */ @Documented @Retention(RetentionPolicy.SOURCE) @IntDef( flag = true, - value = {FLAG_ALLOW_GZIP, FLAG_ALLOW_CACHING_UNKNOWN_LENGTH}) + value = {FLAG_ALLOW_GZIP, FLAG_DONT_CACHE_IF_LENGTH_UNKNOWN}) public @interface Flags {} /** - * Permits an underlying network stack to request that the server use gzip compression. - *

- * Should not typically be set if the data being requested is already compressed (e.g. most audio - * and video requests). May be set when requesting other data. - *

- * When a {@link DataSource} is used to request data with this flag set, and if the - * {@link DataSource} does make a network request, then the value returned from - * {@link DataSource#open(DataSpec)} will typically be {@link C#LENGTH_UNSET}. The data read from - * {@link DataSource#read(byte[], int, int)} will be the decompressed data. + * Allows an underlying network stack to request that the server use gzip compression. + * + *

Should not typically be set if the data being requested is already compressed (e.g. most + * audio and video requests). May be set when requesting other data. + * + *

When a {@link DataSource} is used to request data with this flag set, and if the {@link + * DataSource} does make a network request, then the value returned from {@link + * DataSource#open(DataSpec)} will typically be {@link C#LENGTH_UNSET}. The data read from {@link + * DataSource#read(byte[], int, int)} will be the decompressed data. */ public static final int FLAG_ALLOW_GZIP = 1; - /** - * Permits content to be cached even if its length can not be resolved. Typically this's the case - * for progressive live streams and when {@link #FLAG_ALLOW_GZIP} is used. - */ - public static final int FLAG_ALLOW_CACHING_UNKNOWN_LENGTH = 1 << 1; // 2 + /** Prevents caching if the length cannot be resolved when the {@link DataSource} is opened. */ + public static final int FLAG_DONT_CACHE_IF_LENGTH_UNKNOWN = 1 << 1; // 2 /** * The set of HTTP methods that are supported by ExoPlayer {@link HttpDataSource}s. One of {@link diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/ParsingLoadable.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/ParsingLoadable.java index cdcb3787fa..b41f1aa09f 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/ParsingLoadable.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/ParsingLoadable.java @@ -69,6 +69,24 @@ public final class ParsingLoadable implements Loadable { return Assertions.checkNotNull(loadable.getResult()); } + /** + * Loads a single parsable object. + * + * @param dataSource The {@link DataSource} through which the object should be read. + * @param parser The {@link Parser} to parse the object from the response. + * @param dataSpec The {@link DataSpec} of the object to read. + * @param type The type of the data. One of the {@link C}{@code DATA_TYPE_*} constants. + * @return The parsed object + * @throws IOException Thrown if there is an error while loading or parsing. + */ + public static T load( + DataSource dataSource, Parser parser, DataSpec dataSpec, int type) + throws IOException { + ParsingLoadable loadable = new ParsingLoadable<>(dataSource, dataSpec, type, parser); + loadable.load(); + return Assertions.checkNotNull(loadable.getResult()); + } + /** * The {@link DataSpec} that defines the data to be loaded. */ @@ -91,11 +109,7 @@ public final class ParsingLoadable implements Loadable { * @param parser Parses the object from the response. */ public ParsingLoadable(DataSource dataSource, Uri uri, int type, Parser parser) { - this( - dataSource, - new DataSpec(uri, DataSpec.FLAG_ALLOW_GZIP | DataSpec.FLAG_ALLOW_CACHING_UNKNOWN_LENGTH), - type, - parser); + this(dataSource, new DataSpec(uri, DataSpec.FLAG_ALLOW_GZIP), type, parser); } /** diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSink.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSink.java index 8d310015f8..e9c3379280 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSink.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheDataSink.java @@ -121,7 +121,7 @@ public final class CacheDataSink implements DataSink { @Override public void open(DataSpec dataSpec) throws CacheDataSinkException { if (dataSpec.length == C.LENGTH_UNSET - && !dataSpec.isFlagSet(DataSpec.FLAG_ALLOW_CACHING_UNKNOWN_LENGTH)) { + && dataSpec.isFlagSet(DataSpec.FLAG_DONT_CACHE_IF_LENGTH_UNKNOWN)) { this.dataSpec = null; return; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheUtil.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheUtil.java index 1a44fb3144..fd4937ef86 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheUtil.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/cache/CacheUtil.java @@ -268,7 +268,7 @@ public final class CacheUtil { dataSpec.position + absoluteStreamPosition - dataSpec.absoluteStreamPosition, C.LENGTH_UNSET, dataSpec.key, - dataSpec.flags | DataSpec.FLAG_ALLOW_CACHING_UNKNOWN_LENGTH); + dataSpec.flags); long resolvedLength = dataSource.open(dataSpec); if (counters.contentLength == C.LENGTH_UNSET && resolvedLength != C.LENGTH_UNSET) { counters.contentLength = dataSpec.absoluteStreamPosition + resolvedLength; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/Util.java b/library/core/src/main/java/com/google/android/exoplayer2/util/Util.java index 9c2f426de5..7bea5de8ba 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/Util.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/Util.java @@ -48,8 +48,15 @@ import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlayerLibraryInfo; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.ParserException; +import com.google.android.exoplayer2.Renderer; +import com.google.android.exoplayer2.RendererCapabilities; +import com.google.android.exoplayer2.RenderersFactory; import com.google.android.exoplayer2.SeekParameters; +import com.google.android.exoplayer2.audio.AudioRendererEventListener; +import com.google.android.exoplayer2.drm.DrmSessionManager; +import com.google.android.exoplayer2.drm.FrameworkMediaCrypto; import com.google.android.exoplayer2.upstream.DataSource; +import com.google.android.exoplayer2.video.VideoRendererEventListener; import java.io.ByteArrayOutputStream; import java.io.Closeable; import java.io.File; @@ -1842,6 +1849,32 @@ public final class Util { return displaySize; } + /** + * Extract renderer capabilities for the renderers created by the provided renderers factory. + * + * @param renderersFactory A {@link RenderersFactory}. + * @param drmSessionManager An optional {@link DrmSessionManager} used by the renderers. + * @return The {@link RendererCapabilities} for each renderer created by the {@code + * renderersFactory}. + */ + public static RendererCapabilities[] getRendererCapabilities( + RenderersFactory renderersFactory, + @Nullable DrmSessionManager drmSessionManager) { + Renderer[] renderers = + renderersFactory.createRenderers( + new Handler(), + new VideoRendererEventListener() {}, + new AudioRendererEventListener() {}, + (cues) -> {}, + (metadata) -> {}, + drmSessionManager); + RendererCapabilities[] capabilities = new RendererCapabilities[renderers.length]; + for (int i = 0; i < renderers.length; i++) { + capabilities[i] = renderers[i].getCapabilities(); + } + return capabilities; + } + @Nullable private static String getSystemProperty(String name) { try { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/video/VideoRendererEventListener.java b/library/core/src/main/java/com/google/android/exoplayer2/video/VideoRendererEventListener.java index 617211afb7..7d78ba03c7 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/video/VideoRendererEventListener.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/video/VideoRendererEventListener.java @@ -26,7 +26,8 @@ import com.google.android.exoplayer2.decoder.DecoderCounters; import com.google.android.exoplayer2.util.Assertions; /** - * Listener of video {@link Renderer} events. + * Listener of video {@link Renderer} events. All methods have no-op default implementations to + * allow selective overrides. */ public interface VideoRendererEventListener { @@ -36,7 +37,7 @@ public interface VideoRendererEventListener { * @param counters {@link DecoderCounters} that will be updated by the renderer for as long as it * remains enabled. */ - void onVideoEnabled(DecoderCounters counters); + default void onVideoEnabled(DecoderCounters counters) {} /** * Called when a decoder is created. @@ -46,15 +47,15 @@ public interface VideoRendererEventListener { * finished. * @param initializationDurationMs The time taken to initialize the decoder in milliseconds. */ - void onVideoDecoderInitialized(String decoderName, long initializedTimestampMs, - long initializationDurationMs); + default void onVideoDecoderInitialized( + String decoderName, long initializedTimestampMs, long initializationDurationMs) {} /** * Called when the format of the media being consumed by the renderer changes. * * @param format The new format. */ - void onVideoInputFormatChanged(Format format); + default void onVideoInputFormatChanged(Format format) {} /** * Called to report the number of frames dropped by the renderer. Dropped frames are reported @@ -62,12 +63,11 @@ public interface VideoRendererEventListener { * reaches a specified threshold whilst the renderer is started. * * @param count The number of dropped frames. - * @param elapsedMs The duration in milliseconds over which the frames were dropped. This - * duration is timed from when the renderer was started or from when dropped frames were - * last reported (whichever was more recent), and not from when the first of the reported - * drops occurred. + * @param elapsedMs The duration in milliseconds over which the frames were dropped. This duration + * is timed from when the renderer was started or from when dropped frames were last reported + * (whichever was more recent), and not from when the first of the reported drops occurred. */ - void onDroppedFrames(int count, long elapsedMs); + default void onDroppedFrames(int count, long elapsedMs) {} /** * Called before a frame is rendered for the first time since setting the surface, and each time @@ -82,12 +82,12 @@ public interface VideoRendererEventListener { * this is not possible. Applications that use {@link TextureView} can apply the rotation by * calling {@link TextureView#setTransform}. Applications that do not expect to encounter * rotated videos can safely ignore this parameter. - * @param pixelWidthHeightRatio The width to height ratio of each pixel. For the normal case - * of square pixels this will be equal to 1.0. Different values are indicative of anamorphic + * @param pixelWidthHeightRatio The width to height ratio of each pixel. For the normal case of + * square pixels this will be equal to 1.0. Different values are indicative of anamorphic * content. */ - void onVideoSizeChanged(int width, int height, int unappliedRotationDegrees, - float pixelWidthHeightRatio); + default void onVideoSizeChanged( + int width, int height, int unappliedRotationDegrees, float pixelWidthHeightRatio) {} /** * Called when a frame is rendered for the first time since setting the surface, and when a frame @@ -96,14 +96,14 @@ public interface VideoRendererEventListener { * @param surface The {@link Surface} to which a first frame has been rendered, or {@code null} if * the renderer renders to something that isn't a {@link Surface}. */ - void onRenderedFirstFrame(@Nullable Surface surface); + default void onRenderedFirstFrame(@Nullable Surface surface) {} /** * Called when the renderer is disabled. * * @param counters {@link DecoderCounters} that were updated by the renderer. */ - void onVideoDisabled(DecoderCounters counters); + default void onVideoDisabled(DecoderCounters counters) {} /** * Dispatches events to a {@link VideoRendererEventListener}. diff --git a/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CacheDataSourceTest.java b/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CacheDataSourceTest.java index 83126ce34a..9182074eb9 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CacheDataSourceTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/upstream/cache/CacheDataSourceTest.java @@ -602,7 +602,6 @@ public final class CacheDataSourceTest { } private DataSpec buildDataSpec(long position, long length, @Nullable String key) { - return new DataSpec( - testDataUri, position, length, key, DataSpec.FLAG_ALLOW_CACHING_UNKNOWN_LENGTH); + return new DataSpec(testDataUri, position, length, key); } } diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java index b04fcf7247..1f08a43731 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java @@ -607,6 +607,12 @@ public final class DashMediaSource extends BaseMediaSource { // MediaSource implementation. + @Override + @Nullable + public Object getTag() { + return tag; + } + @Override public void prepareSourceInternal( ExoPlayer player, diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/offline/DashDownloadHelper.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/offline/DashDownloadHelper.java index 9c6a24d1b2..f4e43f4641 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/offline/DashDownloadHelper.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/offline/DashDownloadHelper.java @@ -16,7 +16,6 @@ package com.google.android.exoplayer2.source.dash.offline; import android.net.Uri; -import android.support.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.offline.DownloadAction; @@ -31,74 +30,49 @@ import com.google.android.exoplayer2.source.dash.manifest.DashManifestParser; import com.google.android.exoplayer2.source.dash.manifest.Representation; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.ParsingLoadable; -import com.google.android.exoplayer2.util.Assertions; import java.io.IOException; import java.util.ArrayList; import java.util.List; -import org.checkerframework.checker.nullness.qual.MonotonicNonNull; /** A {@link DownloadHelper} for DASH streams. */ -public final class DashDownloadHelper extends DownloadHelper { +public final class DashDownloadHelper extends DownloadHelper { - private final Uri uri; private final DataSource.Factory manifestDataSourceFactory; - private @MonotonicNonNull DashManifest manifest; - public DashDownloadHelper(Uri uri, DataSource.Factory manifestDataSourceFactory) { - this.uri = uri; + super(DownloadAction.TYPE_DASH, uri, /* cacheKey= */ null); this.manifestDataSourceFactory = manifestDataSourceFactory; } @Override - protected void prepareInternal() throws IOException { + protected DashManifest loadManifest(Uri uri) throws IOException { DataSource dataSource = manifestDataSourceFactory.createDataSource(); - manifest = - ParsingLoadable.load(dataSource, new DashManifestParser(), uri, C.DATA_TYPE_MANIFEST); - } - - /** Returns the DASH manifest. Must not be called until after preparation completes. */ - public DashManifest getManifest() { - Assertions.checkNotNull(manifest); - return manifest; + return ParsingLoadable.load(dataSource, new DashManifestParser(), uri, C.DATA_TYPE_MANIFEST); } @Override - public int getPeriodCount() { - Assertions.checkNotNull(manifest); - return manifest.getPeriodCount(); - } - - @Override - public TrackGroupArray getTrackGroups(int periodIndex) { - Assertions.checkNotNull(manifest); - List adaptationSets = manifest.getPeriod(periodIndex).adaptationSets; - TrackGroup[] trackGroups = new TrackGroup[adaptationSets.size()]; - for (int i = 0; i < trackGroups.length; i++) { - List representations = adaptationSets.get(i).representations; - Format[] formats = new Format[representations.size()]; - int representationsCount = representations.size(); - for (int j = 0; j < representationsCount; j++) { - formats[j] = representations.get(j).format; + public TrackGroupArray[] getTrackGroupArrays(DashManifest manifest) { + int periodCount = manifest.getPeriodCount(); + TrackGroupArray[] trackGroupArrays = new TrackGroupArray[periodCount]; + for (int periodIndex = 0; periodIndex < periodCount; periodIndex++) { + List adaptationSets = manifest.getPeriod(periodIndex).adaptationSets; + TrackGroup[] trackGroups = new TrackGroup[adaptationSets.size()]; + for (int i = 0; i < trackGroups.length; i++) { + List representations = adaptationSets.get(i).representations; + Format[] formats = new Format[representations.size()]; + int representationsCount = representations.size(); + for (int j = 0; j < representationsCount; j++) { + formats[j] = representations.get(j).format; + } + trackGroups[i] = new TrackGroup(formats); } - trackGroups[i] = new TrackGroup(formats); + trackGroupArrays[periodIndex] = new TrackGroupArray(trackGroups); } - return new TrackGroupArray(trackGroups); + return trackGroupArrays; } @Override - public DownloadAction getDownloadAction(@Nullable byte[] data, List trackKeys) { - return DownloadAction.createDownloadAction( - DownloadAction.TYPE_DASH, uri, toStreamKeys(trackKeys), /* customCacheKey= */ null, data); - } - - @Override - public DownloadAction getRemoveAction() { - return DownloadAction.createRemoveAction( - DownloadAction.TYPE_DASH, uri, /* customCacheKey= */ null); - } - - private static List toStreamKeys(List trackKeys) { + protected List toStreamKeys(List trackKeys) { List streamKeys = new ArrayList<>(trackKeys.size()); for (int i = 0; i < trackKeys.size(); i++) { TrackKey trackKey = trackKeys.get(i); diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/offline/DashDownloader.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/offline/DashDownloader.java index 68120d6177..5dad468724 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/offline/DashDownloader.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/offline/DashDownloader.java @@ -28,11 +28,13 @@ import com.google.android.exoplayer2.source.dash.DashUtil; import com.google.android.exoplayer2.source.dash.DashWrappingSegmentIndex; import com.google.android.exoplayer2.source.dash.manifest.AdaptationSet; import com.google.android.exoplayer2.source.dash.manifest.DashManifest; +import com.google.android.exoplayer2.source.dash.manifest.DashManifestParser; import com.google.android.exoplayer2.source.dash.manifest.Period; import com.google.android.exoplayer2.source.dash.manifest.RangedUri; import com.google.android.exoplayer2.source.dash.manifest.Representation; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DataSpec; +import com.google.android.exoplayer2.upstream.ParsingLoadable; import java.io.IOException; import java.util.ArrayList; import java.util.List; @@ -73,8 +75,9 @@ public final class DashDownloader extends SegmentDownloader { } @Override - protected DashManifest getManifest(DataSource dataSource, Uri uri) throws IOException { - return DashUtil.loadManifest(dataSource, uri); + protected DashManifest getManifest(DataSource dataSource, DataSpec dataSpec) throws IOException { + return ParsingLoadable.load( + dataSource, new DashManifestParser(), dataSpec, C.DATA_TYPE_MANIFEST); } @Override @@ -121,8 +124,7 @@ public final class DashDownloader extends SegmentDownloader { if (!allowIncompleteList) { throw e; } - // Loading failed, but generating an incomplete segment list is allowed. Advance to the next - // representation. + // Generating an incomplete segment list is allowed. Advance to the next representation. continue; } diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaSource.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaSource.java index a075dacf3a..a9b0c579ac 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaSource.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaSource.java @@ -390,6 +390,12 @@ public final class HlsMediaSource extends BaseMediaSource this.tag = tag; } + @Override + @Nullable + public Object getTag() { + return tag; + } + @Override public void prepareSourceInternal( ExoPlayer player, diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloadHelper.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloadHelper.java index d4cbd8b638..c6ebe8e294 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloadHelper.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloadHelper.java @@ -16,7 +16,6 @@ package com.google.android.exoplayer2.source.hls.offline; import android.net.Uri; -import android.support.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.offline.DownloadAction; @@ -36,46 +35,31 @@ import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.List; -import org.checkerframework.checker.nullness.qual.MonotonicNonNull; /** A {@link DownloadHelper} for HLS streams. */ -public final class HlsDownloadHelper extends DownloadHelper { +public final class HlsDownloadHelper extends DownloadHelper { - private final Uri uri; private final DataSource.Factory manifestDataSourceFactory; - private @MonotonicNonNull HlsPlaylist playlist; private int[] renditionGroups; public HlsDownloadHelper(Uri uri, DataSource.Factory manifestDataSourceFactory) { - this.uri = uri; + super(DownloadAction.TYPE_HLS, uri, /* cacheKey= */ null); this.manifestDataSourceFactory = manifestDataSourceFactory; } @Override - protected void prepareInternal() throws IOException { + protected HlsPlaylist loadManifest(Uri uri) throws IOException { DataSource dataSource = manifestDataSourceFactory.createDataSource(); - playlist = ParsingLoadable.load(dataSource, new HlsPlaylistParser(), uri, C.DATA_TYPE_MANIFEST); - } - - /** Returns the HLS playlist. Must not be called until after preparation completes. */ - public HlsPlaylist getPlaylist() { - Assertions.checkNotNull(playlist); - return playlist; + return ParsingLoadable.load(dataSource, new HlsPlaylistParser(), uri, C.DATA_TYPE_MANIFEST); } @Override - public int getPeriodCount() { - Assertions.checkNotNull(playlist); - return 1; - } - - @Override - public TrackGroupArray getTrackGroups(int periodIndex) { + protected TrackGroupArray[] getTrackGroupArrays(HlsPlaylist playlist) { Assertions.checkNotNull(playlist); if (playlist instanceof HlsMediaPlaylist) { renditionGroups = new int[0]; - return TrackGroupArray.EMPTY; + return new TrackGroupArray[] {TrackGroupArray.EMPTY}; } // TODO: Generate track groups as in playback. Reverse the mapping in getDownloadAction. HlsMasterPlaylist masterPlaylist = (HlsMasterPlaylist) playlist; @@ -94,24 +78,18 @@ public final class HlsDownloadHelper extends DownloadHelper { renditionGroups[trackGroupIndex] = HlsMasterPlaylist.GROUP_INDEX_SUBTITLE; trackGroups[trackGroupIndex++] = new TrackGroup(toFormats(masterPlaylist.subtitles)); } - return new TrackGroupArray(Arrays.copyOf(trackGroups, trackGroupIndex)); + return new TrackGroupArray[] {new TrackGroupArray(Arrays.copyOf(trackGroups, trackGroupIndex))}; } @Override - public DownloadAction getDownloadAction(@Nullable byte[] data, List trackKeys) { - Assertions.checkNotNull(renditionGroups); - return DownloadAction.createDownloadAction( - DownloadAction.TYPE_HLS, - uri, - toStreamKeys(trackKeys, renditionGroups), - /* customCacheKey= */ null, - data); - } - - @Override - public DownloadAction getRemoveAction() { - return DownloadAction.createRemoveAction( - DownloadAction.TYPE_HLS, uri, /* customCacheKey= */ null); + protected List toStreamKeys(List trackKeys) { + List representationKeys = new ArrayList<>(trackKeys.size()); + for (int i = 0; i < trackKeys.size(); i++) { + TrackKey trackKey = trackKeys.get(i); + representationKeys.add( + new StreamKey(renditionGroups[trackKey.groupIndex], trackKey.trackIndex)); + } + return representationKeys; } private static Format[] toFormats(List hlsUrls) { @@ -121,13 +99,4 @@ public final class HlsDownloadHelper extends DownloadHelper { } return formats; } - - private static List toStreamKeys(List trackKeys, int[] groups) { - List representationKeys = new ArrayList<>(trackKeys.size()); - for (int i = 0; i < trackKeys.size(); i++) { - TrackKey trackKey = trackKeys.get(i); - representationKeys.add(new StreamKey(groups[trackKey.groupIndex], trackKey.trackIndex)); - } - return representationKeys; - } } diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloader.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloader.java index 85f41df359..a0f64f298e 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloader.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/offline/HlsDownloader.java @@ -71,35 +71,37 @@ public final class HlsDownloader extends SegmentDownloader { } @Override - protected HlsPlaylist getManifest(DataSource dataSource, Uri uri) throws IOException { - return loadManifest(dataSource, uri); + protected HlsPlaylist getManifest(DataSource dataSource, DataSpec dataSpec) throws IOException { + return loadManifest(dataSource, dataSpec); } @Override protected List getSegments( DataSource dataSource, HlsPlaylist playlist, boolean allowIncompleteList) throws IOException { - ArrayList mediaPlaylistUris = new ArrayList<>(); + String baseUri = playlist.baseUri; + + ArrayList mediaPlaylistDataSpecs = new ArrayList<>(); if (playlist instanceof HlsMasterPlaylist) { HlsMasterPlaylist masterPlaylist = (HlsMasterPlaylist) playlist; - addResolvedUris(masterPlaylist.baseUri, masterPlaylist.variants, mediaPlaylistUris); - addResolvedUris(masterPlaylist.baseUri, masterPlaylist.audios, mediaPlaylistUris); - addResolvedUris(masterPlaylist.baseUri, masterPlaylist.subtitles, mediaPlaylistUris); + addMediaPlaylistDataSpecs(baseUri, masterPlaylist.variants, mediaPlaylistDataSpecs); + addMediaPlaylistDataSpecs(baseUri, masterPlaylist.audios, mediaPlaylistDataSpecs); + addMediaPlaylistDataSpecs(baseUri, masterPlaylist.subtitles, mediaPlaylistDataSpecs); } else { - mediaPlaylistUris.add(Uri.parse(playlist.baseUri)); + mediaPlaylistDataSpecs.add(SegmentDownloader.getCompressibleDataSpec(Uri.parse(baseUri))); } - ArrayList segments = new ArrayList<>(); + ArrayList segments = new ArrayList<>(); HashSet seenEncryptionKeyUris = new HashSet<>(); - for (Uri mediaPlaylistUri : mediaPlaylistUris) { + for (DataSpec mediaPlaylistDataSpec : mediaPlaylistDataSpecs) { + segments.add(new Segment(/* startTimeUs= */ 0, mediaPlaylistDataSpec)); HlsMediaPlaylist mediaPlaylist; try { - mediaPlaylist = (HlsMediaPlaylist) loadManifest(dataSource, mediaPlaylistUri); - segments.add(new Segment(mediaPlaylist.startTimeUs, new DataSpec(mediaPlaylistUri))); + mediaPlaylist = (HlsMediaPlaylist) loadManifest(dataSource, mediaPlaylistDataSpec); } catch (IOException e) { if (!allowIncompleteList) { throw e; } - segments.add(new Segment(0, new DataSpec(mediaPlaylistUri))); + // Generating an incomplete segment list is allowed. Advance to the next media playlist. continue; } HlsMediaPlaylist.Segment lastInitSegment = null; @@ -109,39 +111,43 @@ public final class HlsDownloader extends SegmentDownloader { HlsMediaPlaylist.Segment initSegment = segment.initializationSegment; if (initSegment != null && initSegment != lastInitSegment) { lastInitSegment = initSegment; - addSegment(segments, mediaPlaylist, initSegment, seenEncryptionKeyUris); + addSegment(mediaPlaylist, initSegment, seenEncryptionKeyUris, segments); } - addSegment(segments, mediaPlaylist, segment, seenEncryptionKeyUris); + addSegment(mediaPlaylist, segment, seenEncryptionKeyUris, segments); } } return segments; } - private static HlsPlaylist loadManifest(DataSource dataSource, Uri uri) throws IOException { - return ParsingLoadable.load(dataSource, new HlsPlaylistParser(), uri, C.DATA_TYPE_MANIFEST); + private void addMediaPlaylistDataSpecs(String baseUri, List urls, List out) { + for (int i = 0; i < urls.size(); i++) { + Uri playlistUri = UriUtil.resolveToUri(baseUri, urls.get(i).url); + out.add(SegmentDownloader.getCompressibleDataSpec(playlistUri)); + } } - private static void addSegment( - ArrayList segments, + private static HlsPlaylist loadManifest(DataSource dataSource, DataSpec dataSpec) + throws IOException { + return ParsingLoadable.load( + dataSource, new HlsPlaylistParser(), dataSpec, C.DATA_TYPE_MANIFEST); + } + + private void addSegment( HlsMediaPlaylist mediaPlaylist, - HlsMediaPlaylist.Segment hlsSegment, - HashSet seenEncryptionKeyUris) { - long startTimeUs = mediaPlaylist.startTimeUs + hlsSegment.relativeStartTimeUs; - if (hlsSegment.fullSegmentEncryptionKeyUri != null) { - Uri keyUri = UriUtil.resolveToUri(mediaPlaylist.baseUri, - hlsSegment.fullSegmentEncryptionKeyUri); + HlsMediaPlaylist.Segment segment, + HashSet seenEncryptionKeyUris, + ArrayList out) { + String baseUri = mediaPlaylist.baseUri; + long startTimeUs = mediaPlaylist.startTimeUs + segment.relativeStartTimeUs; + if (segment.fullSegmentEncryptionKeyUri != null) { + Uri keyUri = UriUtil.resolveToUri(baseUri, segment.fullSegmentEncryptionKeyUri); if (seenEncryptionKeyUris.add(keyUri)) { - segments.add(new Segment(startTimeUs, new DataSpec(keyUri))); + out.add(new Segment(startTimeUs, SegmentDownloader.getCompressibleDataSpec(keyUri))); } } - Uri resolvedUri = UriUtil.resolveToUri(mediaPlaylist.baseUri, hlsSegment.url); - segments.add(new Segment(startTimeUs, - new DataSpec(resolvedUri, hlsSegment.byterangeOffset, hlsSegment.byterangeLength, null))); - } - - private static void addResolvedUris(String baseUri, List urls, List out) { - for (int i = 0; i < urls.size(); i++) { - out.add(UriUtil.resolveToUri(baseUri, urls.get(i).url)); - } + Uri segmentUri = UriUtil.resolveToUri(baseUri, segment.url); + DataSpec dataSpec = + new DataSpec(segmentUri, segment.byterangeOffset, segment.byterangeLength, /* key= */ null); + out.add(new Segment(startTimeUs, dataSpec)); } } diff --git a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaSource.java b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaSource.java index a756b7f4f1..103a52a55a 100644 --- a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaSource.java +++ b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaSource.java @@ -503,6 +503,12 @@ public final class SsMediaSource extends BaseMediaSource // MediaSource implementation. + @Override + @Nullable + public Object getTag() { + return tag; + } + @Override public void prepareSourceInternal( ExoPlayer player, diff --git a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/offline/SsDownloadHelper.java b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/offline/SsDownloadHelper.java index d7083400cd..154ac30ac6 100644 --- a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/offline/SsDownloadHelper.java +++ b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/offline/SsDownloadHelper.java @@ -16,7 +16,6 @@ package com.google.android.exoplayer2.source.smoothstreaming.offline; import android.net.Uri; -import android.support.annotation.Nullable; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.offline.DownloadAction; import com.google.android.exoplayer2.offline.DownloadHelper; @@ -28,67 +27,38 @@ import com.google.android.exoplayer2.source.smoothstreaming.manifest.SsManifest; import com.google.android.exoplayer2.source.smoothstreaming.manifest.SsManifestParser; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.ParsingLoadable; -import com.google.android.exoplayer2.util.Assertions; import java.io.IOException; import java.util.ArrayList; import java.util.List; -import org.checkerframework.checker.nullness.qual.MonotonicNonNull; /** A {@link DownloadHelper} for SmoothStreaming streams. */ -public final class SsDownloadHelper extends DownloadHelper { +public final class SsDownloadHelper extends DownloadHelper { - private final Uri uri; private final DataSource.Factory manifestDataSourceFactory; - private @MonotonicNonNull SsManifest manifest; - public SsDownloadHelper(Uri uri, DataSource.Factory manifestDataSourceFactory) { - this.uri = uri; + super(DownloadAction.TYPE_SS, uri, /* cacheKey= */ null); this.manifestDataSourceFactory = manifestDataSourceFactory; } @Override - protected void prepareInternal() throws IOException { + protected SsManifest loadManifest(Uri uri) throws IOException { DataSource dataSource = manifestDataSourceFactory.createDataSource(); - manifest = ParsingLoadable.load(dataSource, new SsManifestParser(), uri, C.DATA_TYPE_MANIFEST); - } - - /** Returns the SmoothStreaming manifest. Must not be called until after preparation completes. */ - public SsManifest getManifest() { - Assertions.checkNotNull(manifest); - return manifest; + return ParsingLoadable.load(dataSource, new SsManifestParser(), uri, C.DATA_TYPE_MANIFEST); } @Override - public int getPeriodCount() { - Assertions.checkNotNull(manifest); - return 1; - } - - @Override - public TrackGroupArray getTrackGroups(int periodIndex) { - Assertions.checkNotNull(manifest); + protected TrackGroupArray[] getTrackGroupArrays(SsManifest manifest) { SsManifest.StreamElement[] streamElements = manifest.streamElements; TrackGroup[] trackGroups = new TrackGroup[streamElements.length]; for (int i = 0; i < streamElements.length; i++) { trackGroups[i] = new TrackGroup(streamElements[i].formats); } - return new TrackGroupArray(trackGroups); + return new TrackGroupArray[] {new TrackGroupArray(trackGroups)}; } @Override - public DownloadAction getDownloadAction(@Nullable byte[] data, List trackKeys) { - return DownloadAction.createDownloadAction( - DownloadAction.TYPE_SS, uri, toStreamKeys(trackKeys), /* customCacheKey= */ null, data); - } - - @Override - public DownloadAction getRemoveAction() { - return DownloadAction.createRemoveAction( - DownloadAction.TYPE_SS, uri, /* customCacheKey= */ null); - } - - private static List toStreamKeys(List trackKeys) { + protected List toStreamKeys(List trackKeys) { List representationKeys = new ArrayList<>(trackKeys.size()); for (int i = 0; i < trackKeys.size(); i++) { TrackKey trackKey = trackKeys.get(i); diff --git a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/offline/SsDownloader.java b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/offline/SsDownloader.java index 84ef251e5f..18820ca49c 100644 --- a/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/offline/SsDownloader.java +++ b/library/smoothstreaming/src/main/java/com/google/android/exoplayer2/source/smoothstreaming/offline/SsDownloader.java @@ -68,8 +68,8 @@ public final class SsDownloader extends SegmentDownloader { } @Override - protected SsManifest getManifest(DataSource dataSource, Uri uri) throws IOException { - return ParsingLoadable.load(dataSource, new SsManifestParser(), uri, C.DATA_TYPE_MANIFEST); + protected SsManifest getManifest(DataSource dataSource, DataSpec dataSpec) throws IOException { + return ParsingLoadable.load(dataSource, new SsManifestParser(), dataSpec, C.DATA_TYPE_MANIFEST); } @Override diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaSource.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaSource.java index 2fca4f42c7..1f0c0c1a40 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaSource.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeMediaSource.java @@ -88,6 +88,13 @@ public class FakeMediaSource extends BaseMediaSource { this.trackGroupArray = trackGroupArray; } + @Override + @Nullable + public Object getTag() { + boolean hasTimeline = timeline != null && !timeline.isEmpty(); + return hasTimeline ? timeline.getWindow(0, new Timeline.Window()).tag : null; + } + @Override public synchronized void prepareSourceInternal( ExoPlayer player, diff --git a/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/CacheAsserts.java b/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/CacheAsserts.java index 9d6fbe37e7..664532d3ff 100644 --- a/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/CacheAsserts.java +++ b/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/CacheAsserts.java @@ -83,7 +83,7 @@ public final class CacheAsserts { * @throws IOException If an error occurred reading from the Cache. */ public static void assertDataCached(Cache cache, Uri uri, byte[] expected) throws IOException { - DataSpec dataSpec = new DataSpec(uri, DataSpec.FLAG_ALLOW_CACHING_UNKNOWN_LENGTH); + DataSpec dataSpec = new DataSpec(uri); assertDataCached(cache, dataSpec, expected); }