commit
35e030f56b
@ -1,5 +1,17 @@
|
||||
# Release notes #
|
||||
|
||||
### 2.10.1 ###
|
||||
|
||||
* Offline: Add option to remove all downloads.
|
||||
* HLS: Fix `NullPointerException` when using HLS chunkless preparation
|
||||
([#5868](https://github.com/google/ExoPlayer/issues/5868)).
|
||||
* Fix handling of empty values and line terminators in SHOUTcast ICY metadata
|
||||
([#5876](https://github.com/google/ExoPlayer/issues/5876)).
|
||||
* Fix DVB subtitles for SDK 28
|
||||
([#5862](https://github.com/google/ExoPlayer/issues/5862)).
|
||||
* Add a workaround for a decoder failure on ZTE Axon7 mini devices when playing
|
||||
48kHz audio ([#5821](https://github.com/google/ExoPlayer/issues/5821)).
|
||||
|
||||
### 2.10.0 ###
|
||||
|
||||
* Core library:
|
||||
|
@ -13,8 +13,8 @@
|
||||
// limitations under the License.
|
||||
project.ext {
|
||||
// ExoPlayer version and version code.
|
||||
releaseVersion = '2.10.0'
|
||||
releaseVersionCode = 2010000
|
||||
releaseVersion = '2.10.1'
|
||||
releaseVersionCode = 2010001
|
||||
minSdkVersion = 16
|
||||
targetSdkVersion = 28
|
||||
compileSdkVersion = 28
|
||||
|
@ -30,15 +30,12 @@ import com.google.android.exoplayer2.offline.DownloadIndex;
|
||||
import com.google.android.exoplayer2.offline.DownloadManager;
|
||||
import com.google.android.exoplayer2.offline.DownloadRequest;
|
||||
import com.google.android.exoplayer2.offline.DownloadService;
|
||||
import com.google.android.exoplayer2.offline.StreamKey;
|
||||
import com.google.android.exoplayer2.trackselection.MappingTrackSelector.MappedTrackInfo;
|
||||
import com.google.android.exoplayer2.upstream.DataSource;
|
||||
import com.google.android.exoplayer2.util.Log;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
import java.io.IOException;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CopyOnWriteArraySet;
|
||||
|
||||
/** Tracks media that has been downloaded. */
|
||||
@ -86,11 +83,9 @@ public class DownloadTracker {
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public List<StreamKey> getOfflineStreamKeys(Uri uri) {
|
||||
public DownloadRequest getDownloadRequest(Uri uri) {
|
||||
Download download = downloads.get(uri);
|
||||
return download != null && download.state != Download.STATE_FAILED
|
||||
? download.request.streamKeys
|
||||
: Collections.emptyList();
|
||||
return download != null && download.state != Download.STATE_FAILED ? download.request : null;
|
||||
}
|
||||
|
||||
public void toggleDownload(
|
||||
|
@ -45,7 +45,8 @@ import com.google.android.exoplayer2.drm.HttpMediaDrmCallback;
|
||||
import com.google.android.exoplayer2.drm.UnsupportedDrmException;
|
||||
import com.google.android.exoplayer2.mediacodec.MediaCodecRenderer.DecoderInitializationException;
|
||||
import com.google.android.exoplayer2.mediacodec.MediaCodecUtil.DecoderQueryException;
|
||||
import com.google.android.exoplayer2.offline.StreamKey;
|
||||
import com.google.android.exoplayer2.offline.DownloadHelper;
|
||||
import com.google.android.exoplayer2.offline.DownloadRequest;
|
||||
import com.google.android.exoplayer2.source.BehindLiveWindowException;
|
||||
import com.google.android.exoplayer2.source.ConcatenatingMediaSource;
|
||||
import com.google.android.exoplayer2.source.MediaSource;
|
||||
@ -75,7 +76,6 @@ import java.lang.reflect.Constructor;
|
||||
import java.net.CookieHandler;
|
||||
import java.net.CookieManager;
|
||||
import java.net.CookiePolicy;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
/** An activity that plays media using {@link SimpleExoPlayer}. */
|
||||
@ -457,33 +457,26 @@ public class PlayerActivity extends AppCompatActivity
|
||||
}
|
||||
|
||||
private MediaSource buildMediaSource(Uri uri, @Nullable String overrideExtension) {
|
||||
DownloadRequest downloadRequest =
|
||||
((DemoApplication) getApplication()).getDownloadTracker().getDownloadRequest(uri);
|
||||
if (downloadRequest != null) {
|
||||
return DownloadHelper.createMediaSource(downloadRequest, dataSourceFactory);
|
||||
}
|
||||
@ContentType int type = Util.inferContentType(uri, overrideExtension);
|
||||
List<StreamKey> offlineStreamKeys = getOfflineStreamKeys(uri);
|
||||
switch (type) {
|
||||
case C.TYPE_DASH:
|
||||
return new DashMediaSource.Factory(dataSourceFactory)
|
||||
.setStreamKeys(offlineStreamKeys)
|
||||
.createMediaSource(uri);
|
||||
return new DashMediaSource.Factory(dataSourceFactory).createMediaSource(uri);
|
||||
case C.TYPE_SS:
|
||||
return new SsMediaSource.Factory(dataSourceFactory)
|
||||
.setStreamKeys(offlineStreamKeys)
|
||||
.createMediaSource(uri);
|
||||
return new SsMediaSource.Factory(dataSourceFactory).createMediaSource(uri);
|
||||
case C.TYPE_HLS:
|
||||
return new HlsMediaSource.Factory(dataSourceFactory)
|
||||
.setStreamKeys(offlineStreamKeys)
|
||||
.createMediaSource(uri);
|
||||
return new HlsMediaSource.Factory(dataSourceFactory).createMediaSource(uri);
|
||||
case C.TYPE_OTHER:
|
||||
return new ProgressiveMediaSource.Factory(dataSourceFactory).createMediaSource(uri);
|
||||
default: {
|
||||
default:
|
||||
throw new IllegalStateException("Unsupported type: " + type);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private List<StreamKey> getOfflineStreamKeys(Uri uri) {
|
||||
return ((DemoApplication) getApplication()).getDownloadTracker().getOfflineStreamKeys(uri);
|
||||
}
|
||||
|
||||
private DefaultDrmSessionManager<FrameworkMediaCrypto> buildDrmSessionManagerV18(
|
||||
UUID uuid, String licenseUrl, String[] keyRequestPropertiesArray, boolean multiSession)
|
||||
throws UnsupportedDrmException {
|
||||
|
@ -113,7 +113,7 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
|
||||
|
||||
private final CronetEngine cronetEngine;
|
||||
private final Executor executor;
|
||||
private final Predicate<String> contentTypePredicate;
|
||||
@Nullable private final Predicate<String> contentTypePredicate;
|
||||
private final int connectTimeoutMs;
|
||||
private final int readTimeoutMs;
|
||||
private final boolean resetTimeoutOnRedirects;
|
||||
@ -146,6 +146,18 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
|
||||
|
||||
private volatile long currentConnectTimeoutMs;
|
||||
|
||||
/**
|
||||
* @param cronetEngine A CronetEngine.
|
||||
* @param executor The {@link java.util.concurrent.Executor} that will handle responses. This may
|
||||
* be a direct executor (i.e. executes tasks on the calling thread) in order to avoid a thread
|
||||
* hop from Cronet's internal network thread to the response handling thread. However, to
|
||||
* avoid slowing down overall network performance, care must be taken to make sure response
|
||||
* handling is a fast operation when using a direct executor.
|
||||
*/
|
||||
public CronetDataSource(CronetEngine cronetEngine, Executor executor) {
|
||||
this(cronetEngine, executor, /* contentTypePredicate= */ null);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param cronetEngine A CronetEngine.
|
||||
* @param executor The {@link java.util.concurrent.Executor} that will handle responses. This may
|
||||
@ -158,7 +170,9 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
|
||||
* #open(DataSpec)}.
|
||||
*/
|
||||
public CronetDataSource(
|
||||
CronetEngine cronetEngine, Executor executor, Predicate<String> contentTypePredicate) {
|
||||
CronetEngine cronetEngine,
|
||||
Executor executor,
|
||||
@Nullable Predicate<String> contentTypePredicate) {
|
||||
this(
|
||||
cronetEngine,
|
||||
executor,
|
||||
@ -188,7 +202,7 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
|
||||
public CronetDataSource(
|
||||
CronetEngine cronetEngine,
|
||||
Executor executor,
|
||||
Predicate<String> contentTypePredicate,
|
||||
@Nullable Predicate<String> contentTypePredicate,
|
||||
int connectTimeoutMs,
|
||||
int readTimeoutMs,
|
||||
boolean resetTimeoutOnRedirects,
|
||||
@ -225,7 +239,7 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
|
||||
public CronetDataSource(
|
||||
CronetEngine cronetEngine,
|
||||
Executor executor,
|
||||
Predicate<String> contentTypePredicate,
|
||||
@Nullable Predicate<String> contentTypePredicate,
|
||||
int connectTimeoutMs,
|
||||
int readTimeoutMs,
|
||||
boolean resetTimeoutOnRedirects,
|
||||
@ -246,7 +260,7 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
|
||||
/* package */ CronetDataSource(
|
||||
CronetEngine cronetEngine,
|
||||
Executor executor,
|
||||
Predicate<String> contentTypePredicate,
|
||||
@Nullable Predicate<String> contentTypePredicate,
|
||||
int connectTimeoutMs,
|
||||
int readTimeoutMs,
|
||||
boolean resetTimeoutOnRedirects,
|
||||
|
@ -948,8 +948,8 @@ public final class ImaAdsLoader
|
||||
@Override
|
||||
public void onTimelineChanged(
|
||||
Timeline timeline, @Nullable Object manifest, @Player.TimelineChangeReason int reason) {
|
||||
if (reason == Player.TIMELINE_CHANGE_REASON_RESET) {
|
||||
// The player is being reset and this source will be released.
|
||||
if (timeline.isEmpty()) {
|
||||
// The player is being reset or contains no media.
|
||||
return;
|
||||
}
|
||||
Assertions.checkArgument(timeline.getPeriodCount() == 1);
|
||||
|
@ -73,6 +73,15 @@ public class OkHttpDataSource extends BaseDataSource implements HttpDataSource {
|
||||
private long bytesSkipped;
|
||||
private long bytesRead;
|
||||
|
||||
/**
|
||||
* @param callFactory A {@link Call.Factory} (typically an {@link okhttp3.OkHttpClient}) for use
|
||||
* by the source.
|
||||
* @param userAgent An optional User-Agent string.
|
||||
*/
|
||||
public OkHttpDataSource(Call.Factory callFactory, @Nullable String userAgent) {
|
||||
this(callFactory, userAgent, /* contentTypePredicate= */ null);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param callFactory A {@link Call.Factory} (typically an {@link okhttp3.OkHttpClient}) for use
|
||||
* by the source.
|
||||
|
@ -3,3 +3,4 @@ android.useAndroidX=true
|
||||
android.enableJetifier=true
|
||||
android.enableUnitTestBinaryResources=true
|
||||
buildDir=buildout
|
||||
org.gradle.jvmargs=-Xmx2g -XX:MaxMetaspaceSize=512m
|
||||
|
@ -18,10 +18,13 @@ android.libraryVariants.all { variant ->
|
||||
if (!name.equals("release")) {
|
||||
return; // Skip non-release builds.
|
||||
}
|
||||
def allSourceDirs = variant.sourceSets.inject ([]) {
|
||||
acc, val -> acc << val.javaDirectories
|
||||
}
|
||||
task("generateJavadoc", type: Javadoc) {
|
||||
description = "Generates Javadoc for the ${javadocTitle}."
|
||||
title = "ExoPlayer ${javadocTitle}"
|
||||
source = variant.javaCompileProvider.get().source
|
||||
source = allSourceDirs
|
||||
options {
|
||||
links "http://docs.oracle.com/javase/7/docs/api/"
|
||||
linksOffline "https://developer.android.com/reference",
|
||||
|
@ -46,18 +46,21 @@
|
||||
|
||||
# Constructors accessed via reflection in DownloadHelper
|
||||
-dontnote com.google.android.exoplayer2.source.dash.DashMediaSource$Factory
|
||||
-keepclassmembers class com.google.android.exoplayer2.source.dash.DashMediaSource$Factory {
|
||||
-keepclasseswithmembers class com.google.android.exoplayer2.source.dash.DashMediaSource$Factory {
|
||||
<init>(com.google.android.exoplayer2.upstream.DataSource$Factory);
|
||||
** setStreamKeys(java.util.List);
|
||||
com.google.android.exoplayer2.source.dash.DashMediaSource createMediaSource(android.net.Uri);
|
||||
}
|
||||
-dontnote com.google.android.exoplayer2.source.hls.HlsMediaSource$Factory
|
||||
-keepclassmembers class com.google.android.exoplayer2.source.hls.HlsMediaSource$Factory {
|
||||
-keepclasseswithmembers class com.google.android.exoplayer2.source.hls.HlsMediaSource$Factory {
|
||||
<init>(com.google.android.exoplayer2.upstream.DataSource$Factory);
|
||||
** setStreamKeys(java.util.List);
|
||||
com.google.android.exoplayer2.source.hls.HlsMediaSource createMediaSource(android.net.Uri);
|
||||
}
|
||||
-dontnote com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource$Factory
|
||||
-keepclassmembers class com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource$Factory {
|
||||
-keepclasseswithmembers class com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource$Factory {
|
||||
<init>(com.google.android.exoplayer2.upstream.DataSource$Factory);
|
||||
** setStreamKeys(java.util.List);
|
||||
com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource createMediaSource(android.net.Uri);
|
||||
}
|
||||
|
||||
|
@ -1053,11 +1053,14 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
||||
&& nextInfo.resolvedPeriodIndex == currentPeriodIndex
|
||||
&& nextInfo.resolvedPeriodTimeUs > oldPeriodPositionUs
|
||||
&& nextInfo.resolvedPeriodTimeUs <= newPeriodPositionUs) {
|
||||
sendMessageToTarget(nextInfo.message);
|
||||
if (nextInfo.message.getDeleteAfterDelivery() || nextInfo.message.isCanceled()) {
|
||||
pendingMessages.remove(nextPendingMessageIndex);
|
||||
} else {
|
||||
nextPendingMessageIndex++;
|
||||
try {
|
||||
sendMessageToTarget(nextInfo.message);
|
||||
} finally {
|
||||
if (nextInfo.message.getDeleteAfterDelivery() || nextInfo.message.isCanceled()) {
|
||||
pendingMessages.remove(nextPendingMessageIndex);
|
||||
} else {
|
||||
nextPendingMessageIndex++;
|
||||
}
|
||||
}
|
||||
nextInfo =
|
||||
nextPendingMessageIndex < pendingMessages.size()
|
||||
|
@ -29,11 +29,11 @@ public final class ExoPlayerLibraryInfo {
|
||||
|
||||
/** The version of the library expressed as a string, for example "1.2.3". */
|
||||
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION_INT) or vice versa.
|
||||
public static final String VERSION = "2.10.0";
|
||||
public static final String VERSION = "2.10.1";
|
||||
|
||||
/** The version of the library expressed as {@code "ExoPlayerLib/" + VERSION}. */
|
||||
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa.
|
||||
public static final String VERSION_SLASHY = "ExoPlayerLib/2.10.0";
|
||||
public static final String VERSION_SLASHY = "ExoPlayerLib/2.10.1";
|
||||
|
||||
/**
|
||||
* The version of the library expressed as an integer, for example 1002003.
|
||||
@ -43,7 +43,7 @@ public final class ExoPlayerLibraryInfo {
|
||||
* integer version 123045006 (123-045-006).
|
||||
*/
|
||||
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa.
|
||||
public static final int VERSION_INT = 2010000;
|
||||
public static final int VERSION_INT = 2010001;
|
||||
|
||||
/**
|
||||
* Whether the library was compiled with {@link com.google.android.exoplayer2.util.Assertions}
|
||||
|
@ -1231,7 +1231,7 @@ public class SimpleExoPlayer extends BasePlayer
|
||||
Log.w(
|
||||
TAG,
|
||||
"Player is accessed on the wrong thread. See "
|
||||
+ "https://exoplayer.dev/faqs.html#"
|
||||
+ "https://exoplayer.dev/troubleshooting.html#"
|
||||
+ "what-do-player-is-accessed-on-the-wrong-thread-warnings-mean",
|
||||
hasNotifiedFullWrongThreadWarning ? null : new IllegalStateException());
|
||||
hasNotifiedFullWrongThreadWarning = true;
|
||||
|
@ -59,7 +59,7 @@ public interface AnalyticsListener {
|
||||
public final Timeline timeline;
|
||||
|
||||
/**
|
||||
* Window index in the {@code timeline} this event belongs to, or the prospective window index
|
||||
* Window index in the {@link #timeline} this event belongs to, or the prospective window index
|
||||
* if the timeline is not yet known and empty.
|
||||
*/
|
||||
public final int windowIndex;
|
||||
@ -76,7 +76,7 @@ public interface AnalyticsListener {
|
||||
public final long eventPlaybackPositionMs;
|
||||
|
||||
/**
|
||||
* Position in the current timeline window ({@code timeline.getCurrentWindowIndex()} or the
|
||||
* Position in the current timeline window ({@link Player#getCurrentWindowIndex()}) or the
|
||||
* currently playing ad at the time of the event, in milliseconds.
|
||||
*/
|
||||
public final long currentPlaybackPositionMs;
|
||||
@ -91,15 +91,15 @@ public interface AnalyticsListener {
|
||||
* @param realtimeMs Elapsed real-time as returned by {@code SystemClock.elapsedRealtime()} at
|
||||
* the time of the event, in milliseconds.
|
||||
* @param timeline Timeline at the time of the event.
|
||||
* @param windowIndex Window index in the {@code timeline} this event belongs to, or the
|
||||
* @param windowIndex Window index in the {@link #timeline} this event belongs to, or the
|
||||
* prospective window index if the timeline is not yet known and empty.
|
||||
* @param mediaPeriodId Media period identifier for the media period this event belongs to, or
|
||||
* {@code null} if the event is not associated with a specific media period.
|
||||
* @param eventPlaybackPositionMs Position in the window or ad this event belongs to at the time
|
||||
* of the event, in milliseconds.
|
||||
* @param currentPlaybackPositionMs Position in the current timeline window ({@code
|
||||
* timeline.getCurrentWindowIndex()} or the currently playing ad at the time of the event,
|
||||
* in milliseconds.
|
||||
* @param currentPlaybackPositionMs Position in the current timeline window ({@link
|
||||
* Player#getCurrentWindowIndex()}) or the currently playing ad at the time of the event, in
|
||||
* milliseconds.
|
||||
* @param totalBufferedDurationMs Total buffered duration from {@link
|
||||
* #currentPlaybackPositionMs} at the time of the event, in milliseconds. This includes
|
||||
* pre-buffered data for subsequent ads and windows.
|
||||
|
@ -786,7 +786,7 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
|
||||
// Set codec configuration values.
|
||||
if (Util.SDK_INT >= 23) {
|
||||
mediaFormat.setInteger(MediaFormat.KEY_PRIORITY, 0 /* realtime priority */);
|
||||
if (codecOperatingRate != CODEC_OPERATING_RATE_UNSET) {
|
||||
if (codecOperatingRate != CODEC_OPERATING_RATE_UNSET && !deviceDoesntSupportOperatingRate()) {
|
||||
mediaFormat.setFloat(MediaFormat.KEY_OPERATING_RATE, codecOperatingRate);
|
||||
}
|
||||
}
|
||||
@ -809,6 +809,17 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the device's decoders are known to not support setting the codec operating
|
||||
* rate.
|
||||
*
|
||||
* <p>See <a href="https://github.com/google/ExoPlayer/issues/5821">GitHub issue #5821</a>.
|
||||
*/
|
||||
private static boolean deviceDoesntSupportOperatingRate() {
|
||||
return Util.SDK_INT == 23
|
||||
&& ("ZTE B2017G".equals(Util.MODEL) || "AXON 7 mini".equals(Util.MODEL));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the decoder is known to output six audio channels when provided with input with
|
||||
* fewer than six channels.
|
||||
|
@ -60,7 +60,7 @@ import java.util.List;
|
||||
* The threshold number of samples to trim from the start/end of an audio track when applying an
|
||||
* edit below which gapless info can be used (rather than removing samples from the sample table).
|
||||
*/
|
||||
private static final int MAX_GAPLESS_TRIM_SIZE_SAMPLES = 3;
|
||||
private static final int MAX_GAPLESS_TRIM_SIZE_SAMPLES = 4;
|
||||
|
||||
/** The magic signature for an Opus Identification header, as defined in RFC-7845. */
|
||||
private static final byte[] opusMagic = Util.getUtf8Bytes("OpusHead");
|
||||
|
@ -31,7 +31,7 @@ public final class IcyDecoder implements MetadataDecoder {
|
||||
|
||||
private static final String TAG = "IcyDecoder";
|
||||
|
||||
private static final Pattern METADATA_ELEMENT = Pattern.compile("(.+?)='(.+?)';");
|
||||
private static final Pattern METADATA_ELEMENT = Pattern.compile("(.+?)='(.*?)';", Pattern.DOTALL);
|
||||
private static final String STREAM_KEY_NAME = "streamtitle";
|
||||
private static final String STREAM_KEY_URL = "streamurl";
|
||||
|
||||
|
@ -233,6 +233,19 @@ public final class DefaultDownloadIndex implements WritableDownloadIndex {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setStatesToRemoving() throws DatabaseIOException {
|
||||
ensureInitialized();
|
||||
try {
|
||||
ContentValues values = new ContentValues();
|
||||
values.put(COLUMN_STATE, Download.STATE_REMOVING);
|
||||
SQLiteDatabase writableDatabase = databaseProvider.getWritableDatabase();
|
||||
writableDatabase.update(tableName, values, /* whereClause= */ null, /* whereArgs= */ null);
|
||||
} catch (SQLException e) {
|
||||
throw new DatabaseIOException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setStopReason(int stopReason) throws DatabaseIOException {
|
||||
ensureInitialized();
|
||||
|
@ -20,7 +20,6 @@ import android.os.Handler;
|
||||
import android.os.HandlerThread;
|
||||
import android.os.Message;
|
||||
import androidx.annotation.Nullable;
|
||||
import android.util.Pair;
|
||||
import android.util.SparseIntArray;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.ExoPlaybackException;
|
||||
@ -32,6 +31,7 @@ import com.google.android.exoplayer2.drm.FrameworkMediaCrypto;
|
||||
import com.google.android.exoplayer2.source.MediaPeriod;
|
||||
import com.google.android.exoplayer2.source.MediaSource;
|
||||
import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId;
|
||||
import com.google.android.exoplayer2.source.ProgressiveMediaSource;
|
||||
import com.google.android.exoplayer2.source.TrackGroup;
|
||||
import com.google.android.exoplayer2.source.TrackGroupArray;
|
||||
import com.google.android.exoplayer2.trackselection.BaseTrackSelection;
|
||||
@ -44,6 +44,7 @@ import com.google.android.exoplayer2.trackselection.TrackSelectorResult;
|
||||
import com.google.android.exoplayer2.upstream.Allocator;
|
||||
import com.google.android.exoplayer2.upstream.BandwidthMeter;
|
||||
import com.google.android.exoplayer2.upstream.DataSource;
|
||||
import com.google.android.exoplayer2.upstream.DataSource.Factory;
|
||||
import com.google.android.exoplayer2.upstream.DefaultAllocator;
|
||||
import com.google.android.exoplayer2.upstream.TransferListener;
|
||||
import com.google.android.exoplayer2.util.Assertions;
|
||||
@ -106,30 +107,13 @@ public final class DownloadHelper {
|
||||
void onPrepareError(DownloadHelper helper, IOException e);
|
||||
}
|
||||
|
||||
@Nullable private static final Constructor<?> DASH_FACTORY_CONSTRUCTOR;
|
||||
@Nullable private static final Constructor<?> HLS_FACTORY_CONSTRUCTOR;
|
||||
@Nullable private static final Constructor<?> SS_FACTORY_CONSTRUCTOR;
|
||||
@Nullable private static final Method DASH_FACTORY_CREATE_METHOD;
|
||||
@Nullable private static final Method HLS_FACTORY_CREATE_METHOD;
|
||||
@Nullable private static final Method SS_FACTORY_CREATE_METHOD;
|
||||
|
||||
static {
|
||||
Pair<@NullableType Constructor<?>, @NullableType Method> dashFactoryMethods =
|
||||
getMediaSourceFactoryMethods(
|
||||
"com.google.android.exoplayer2.source.dash.DashMediaSource$Factory");
|
||||
DASH_FACTORY_CONSTRUCTOR = dashFactoryMethods.first;
|
||||
DASH_FACTORY_CREATE_METHOD = dashFactoryMethods.second;
|
||||
Pair<@NullableType Constructor<?>, @NullableType Method> hlsFactoryMethods =
|
||||
getMediaSourceFactoryMethods(
|
||||
"com.google.android.exoplayer2.source.hls.HlsMediaSource$Factory");
|
||||
HLS_FACTORY_CONSTRUCTOR = hlsFactoryMethods.first;
|
||||
HLS_FACTORY_CREATE_METHOD = hlsFactoryMethods.second;
|
||||
Pair<@NullableType Constructor<?>, @NullableType Method> ssFactoryMethods =
|
||||
getMediaSourceFactoryMethods(
|
||||
"com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource$Factory");
|
||||
SS_FACTORY_CONSTRUCTOR = ssFactoryMethods.first;
|
||||
SS_FACTORY_CREATE_METHOD = ssFactoryMethods.second;
|
||||
}
|
||||
private static final MediaSourceFactory DASH_FACTORY =
|
||||
getMediaSourceFactory("com.google.android.exoplayer2.source.dash.DashMediaSource$Factory");
|
||||
private static final MediaSourceFactory SS_FACTORY =
|
||||
getMediaSourceFactory(
|
||||
"com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource$Factory");
|
||||
private static final MediaSourceFactory HLS_FACTORY =
|
||||
getMediaSourceFactory("com.google.android.exoplayer2.source.hls.HlsMediaSource$Factory");
|
||||
|
||||
/**
|
||||
* Creates a {@link DownloadHelper} for progressive streams.
|
||||
@ -202,8 +186,7 @@ public final class DownloadHelper {
|
||||
DownloadRequest.TYPE_DASH,
|
||||
uri,
|
||||
/* cacheKey= */ null,
|
||||
createMediaSource(
|
||||
uri, dataSourceFactory, DASH_FACTORY_CONSTRUCTOR, DASH_FACTORY_CREATE_METHOD),
|
||||
DASH_FACTORY.createMediaSource(uri, dataSourceFactory, /* streamKeys= */ null),
|
||||
trackSelectorParameters,
|
||||
Util.getRendererCapabilities(renderersFactory, drmSessionManager));
|
||||
}
|
||||
@ -252,8 +235,7 @@ public final class DownloadHelper {
|
||||
DownloadRequest.TYPE_HLS,
|
||||
uri,
|
||||
/* cacheKey= */ null,
|
||||
createMediaSource(
|
||||
uri, dataSourceFactory, HLS_FACTORY_CONSTRUCTOR, HLS_FACTORY_CREATE_METHOD),
|
||||
HLS_FACTORY.createMediaSource(uri, dataSourceFactory, /* streamKeys= */ null),
|
||||
trackSelectorParameters,
|
||||
Util.getRendererCapabilities(renderersFactory, drmSessionManager));
|
||||
}
|
||||
@ -302,11 +284,42 @@ public final class DownloadHelper {
|
||||
DownloadRequest.TYPE_SS,
|
||||
uri,
|
||||
/* cacheKey= */ null,
|
||||
createMediaSource(uri, dataSourceFactory, SS_FACTORY_CONSTRUCTOR, SS_FACTORY_CREATE_METHOD),
|
||||
SS_FACTORY.createMediaSource(uri, dataSourceFactory, /* streamKeys= */ null),
|
||||
trackSelectorParameters,
|
||||
Util.getRendererCapabilities(renderersFactory, drmSessionManager));
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility method to create a MediaSource which only contains the tracks defined in {@code
|
||||
* downloadRequest}.
|
||||
*
|
||||
* @param downloadRequest A {@link DownloadRequest}.
|
||||
* @param dataSourceFactory A factory for {@link DataSource}s to read the media.
|
||||
* @return A MediaSource which only contains the tracks defined in {@code downloadRequest}.
|
||||
*/
|
||||
public static MediaSource createMediaSource(
|
||||
DownloadRequest downloadRequest, DataSource.Factory dataSourceFactory) {
|
||||
MediaSourceFactory factory;
|
||||
switch (downloadRequest.type) {
|
||||
case DownloadRequest.TYPE_DASH:
|
||||
factory = DASH_FACTORY;
|
||||
break;
|
||||
case DownloadRequest.TYPE_SS:
|
||||
factory = SS_FACTORY;
|
||||
break;
|
||||
case DownloadRequest.TYPE_HLS:
|
||||
factory = HLS_FACTORY;
|
||||
break;
|
||||
case DownloadRequest.TYPE_PROGRESSIVE:
|
||||
return new ProgressiveMediaSource.Factory(dataSourceFactory)
|
||||
.createMediaSource(downloadRequest.uri);
|
||||
default:
|
||||
throw new IllegalStateException("Unsupported type: " + downloadRequest.type);
|
||||
}
|
||||
return factory.createMediaSource(
|
||||
downloadRequest.uri, dataSourceFactory, downloadRequest.streamKeys);
|
||||
}
|
||||
|
||||
private final String downloadType;
|
||||
private final Uri uri;
|
||||
@Nullable private final String cacheKey;
|
||||
@ -739,35 +752,54 @@ public final class DownloadHelper {
|
||||
}
|
||||
}
|
||||
|
||||
private static Pair<@NullableType Constructor<?>, @NullableType Method>
|
||||
getMediaSourceFactoryMethods(String className) {
|
||||
private static MediaSourceFactory getMediaSourceFactory(String className) {
|
||||
Constructor<?> constructor = null;
|
||||
Method setStreamKeysMethod = null;
|
||||
Method createMethod = null;
|
||||
try {
|
||||
// LINT.IfChange
|
||||
Class<?> factoryClazz = Class.forName(className);
|
||||
constructor = factoryClazz.getConstructor(DataSource.Factory.class);
|
||||
constructor = factoryClazz.getConstructor(Factory.class);
|
||||
setStreamKeysMethod = factoryClazz.getMethod("setStreamKeys", List.class);
|
||||
createMethod = factoryClazz.getMethod("createMediaSource", Uri.class);
|
||||
// LINT.ThenChange(../../../../../../../../proguard-rules.txt)
|
||||
} catch (Exception e) {
|
||||
} catch (ClassNotFoundException e) {
|
||||
// Expected if the app was built without the respective module.
|
||||
} catch (NoSuchMethodException | SecurityException e) {
|
||||
// Something is wrong with the library or the proguard configuration.
|
||||
throw new IllegalStateException(e);
|
||||
}
|
||||
return Pair.create(constructor, createMethod);
|
||||
return new MediaSourceFactory(constructor, setStreamKeysMethod, createMethod);
|
||||
}
|
||||
|
||||
private static MediaSource createMediaSource(
|
||||
Uri uri,
|
||||
DataSource.Factory dataSourceFactory,
|
||||
@Nullable Constructor<?> factoryConstructor,
|
||||
@Nullable Method createMediaSourceMethod) {
|
||||
if (factoryConstructor == null || createMediaSourceMethod == null) {
|
||||
throw new IllegalStateException("Module missing to create media source.");
|
||||
private static final class MediaSourceFactory {
|
||||
@Nullable private final Constructor<?> constructor;
|
||||
@Nullable private final Method setStreamKeysMethod;
|
||||
@Nullable private final Method createMethod;
|
||||
|
||||
public MediaSourceFactory(
|
||||
@Nullable Constructor<?> constructor,
|
||||
@Nullable Method setStreamKeysMethod,
|
||||
@Nullable Method createMethod) {
|
||||
this.constructor = constructor;
|
||||
this.setStreamKeysMethod = setStreamKeysMethod;
|
||||
this.createMethod = createMethod;
|
||||
}
|
||||
try {
|
||||
Object factory = factoryConstructor.newInstance(dataSourceFactory);
|
||||
return (MediaSource) Assertions.checkNotNull(createMediaSourceMethod.invoke(factory, uri));
|
||||
} catch (Exception e) {
|
||||
throw new IllegalStateException("Failed to instantiate media source.", e);
|
||||
|
||||
private MediaSource createMediaSource(
|
||||
Uri uri, Factory dataSourceFactory, @Nullable List<StreamKey> streamKeys) {
|
||||
if (constructor == null || setStreamKeysMethod == null || createMethod == null) {
|
||||
throw new IllegalStateException("Module missing to create media source.");
|
||||
}
|
||||
try {
|
||||
Object factory = constructor.newInstance(dataSourceFactory);
|
||||
if (streamKeys != null) {
|
||||
setStreamKeysMethod.invoke(factory, streamKeys);
|
||||
}
|
||||
return (MediaSource) Assertions.checkNotNull(createMethod.invoke(factory, uri));
|
||||
} catch (Exception e) {
|
||||
throw new IllegalStateException("Failed to instantiate media source.", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -133,10 +133,11 @@ public final class DownloadManager {
|
||||
private static final int MSG_SET_MIN_RETRY_COUNT = 5;
|
||||
private static final int MSG_ADD_DOWNLOAD = 6;
|
||||
private static final int MSG_REMOVE_DOWNLOAD = 7;
|
||||
private static final int MSG_TASK_STOPPED = 8;
|
||||
private static final int MSG_CONTENT_LENGTH_CHANGED = 9;
|
||||
private static final int MSG_UPDATE_PROGRESS = 10;
|
||||
private static final int MSG_RELEASE = 11;
|
||||
private static final int MSG_REMOVE_ALL_DOWNLOADS = 8;
|
||||
private static final int MSG_TASK_STOPPED = 9;
|
||||
private static final int MSG_CONTENT_LENGTH_CHANGED = 10;
|
||||
private static final int MSG_UPDATE_PROGRESS = 11;
|
||||
private static final int MSG_RELEASE = 12;
|
||||
|
||||
private static final String TAG = "DownloadManager";
|
||||
|
||||
@ -446,6 +447,12 @@ public final class DownloadManager {
|
||||
internalHandler.obtainMessage(MSG_REMOVE_DOWNLOAD, id).sendToTarget();
|
||||
}
|
||||
|
||||
/** Cancels all pending downloads and removes all downloaded data. */
|
||||
public void removeAllDownloads() {
|
||||
pendingMessages++;
|
||||
internalHandler.obtainMessage(MSG_REMOVE_ALL_DOWNLOADS).sendToTarget();
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops the downloads and releases resources. Waits until the downloads are persisted to the
|
||||
* download index. The manager must not be accessed after this method has been called.
|
||||
@ -652,6 +659,9 @@ public final class DownloadManager {
|
||||
id = (String) message.obj;
|
||||
removeDownload(id);
|
||||
break;
|
||||
case MSG_REMOVE_ALL_DOWNLOADS:
|
||||
removeAllDownloads();
|
||||
break;
|
||||
case MSG_TASK_STOPPED:
|
||||
Task task = (Task) message.obj;
|
||||
onTaskStopped(task);
|
||||
@ -797,6 +807,36 @@ public final class DownloadManager {
|
||||
syncTasks();
|
||||
}
|
||||
|
||||
private void removeAllDownloads() {
|
||||
List<Download> terminalDownloads = new ArrayList<>();
|
||||
try (DownloadCursor cursor = downloadIndex.getDownloads(STATE_COMPLETED, STATE_FAILED)) {
|
||||
while (cursor.moveToNext()) {
|
||||
terminalDownloads.add(cursor.getDownload());
|
||||
}
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "Failed to load downloads.");
|
||||
}
|
||||
for (int i = 0; i < downloads.size(); i++) {
|
||||
downloads.set(i, copyDownloadWithState(downloads.get(i), STATE_REMOVING));
|
||||
}
|
||||
for (int i = 0; i < terminalDownloads.size(); i++) {
|
||||
downloads.add(copyDownloadWithState(terminalDownloads.get(i), STATE_REMOVING));
|
||||
}
|
||||
Collections.sort(downloads, InternalHandler::compareStartTimes);
|
||||
try {
|
||||
downloadIndex.setStatesToRemoving();
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "Failed to update index.", e);
|
||||
}
|
||||
ArrayList<Download> updateList = new ArrayList<>(downloads);
|
||||
for (int i = 0; i < downloads.size(); i++) {
|
||||
DownloadUpdate update =
|
||||
new DownloadUpdate(downloads.get(i), /* isRemove= */ false, updateList);
|
||||
mainHandler.obtainMessage(MSG_DOWNLOAD_UPDATE, update).sendToTarget();
|
||||
}
|
||||
syncTasks();
|
||||
}
|
||||
|
||||
private void release() {
|
||||
for (Task task : activeTasks.values()) {
|
||||
task.cancel(/* released= */ true);
|
||||
@ -1057,16 +1097,7 @@ public final class DownloadManager {
|
||||
// to set STATE_STOPPED either, because it doesn't have a stopReason argument.
|
||||
Assertions.checkState(
|
||||
state != STATE_COMPLETED && state != STATE_FAILED && state != STATE_STOPPED);
|
||||
return putDownload(
|
||||
new Download(
|
||||
download.request,
|
||||
state,
|
||||
download.startTimeMs,
|
||||
/* updateTimeMs= */ System.currentTimeMillis(),
|
||||
download.contentLength,
|
||||
/* stopReason= */ 0,
|
||||
FAILURE_REASON_NONE,
|
||||
download.progress));
|
||||
return putDownload(copyDownloadWithState(download, state));
|
||||
}
|
||||
|
||||
private Download putDownload(Download download) {
|
||||
@ -1120,6 +1151,18 @@ public final class DownloadManager {
|
||||
return C.INDEX_UNSET;
|
||||
}
|
||||
|
||||
private static Download copyDownloadWithState(Download download, @Download.State int state) {
|
||||
return new Download(
|
||||
download.request,
|
||||
state,
|
||||
download.startTimeMs,
|
||||
/* updateTimeMs= */ System.currentTimeMillis(),
|
||||
download.contentLength,
|
||||
/* stopReason= */ 0,
|
||||
FAILURE_REASON_NONE,
|
||||
download.progress);
|
||||
}
|
||||
|
||||
private static int compareStartTimes(Download first, Download second) {
|
||||
return Util.compareLong(first.startTimeMs, second.startTimeMs);
|
||||
}
|
||||
|
@ -77,6 +77,16 @@ public abstract class DownloadService extends Service {
|
||||
public static final String ACTION_REMOVE_DOWNLOAD =
|
||||
"com.google.android.exoplayer.downloadService.action.REMOVE_DOWNLOAD";
|
||||
|
||||
/**
|
||||
* Removes all downloads. Extras:
|
||||
*
|
||||
* <ul>
|
||||
* <li>{@link #KEY_FOREGROUND} - See {@link #KEY_FOREGROUND}.
|
||||
* </ul>
|
||||
*/
|
||||
public static final String ACTION_REMOVE_ALL_DOWNLOADS =
|
||||
"com.google.android.exoplayer.downloadService.action.REMOVE_ALL_DOWNLOADS";
|
||||
|
||||
/**
|
||||
* Resumes all downloads except those that have a non-zero {@link Download#stopReason}. Extras:
|
||||
*
|
||||
@ -296,6 +306,19 @@ public abstract class DownloadService extends Service {
|
||||
.putExtra(KEY_CONTENT_ID, id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds an {@link Intent} for removing all downloads.
|
||||
*
|
||||
* @param context A {@link Context}.
|
||||
* @param clazz The concrete download service being targeted by the intent.
|
||||
* @param foreground Whether this intent will be used to start the service in the foreground.
|
||||
* @return The created intent.
|
||||
*/
|
||||
public static Intent buildRemoveAllDownloadsIntent(
|
||||
Context context, Class<? extends DownloadService> clazz, boolean foreground) {
|
||||
return getIntent(context, clazz, ACTION_REMOVE_ALL_DOWNLOADS, foreground);
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds an {@link Intent} for resuming all downloads.
|
||||
*
|
||||
@ -414,6 +437,19 @@ public abstract class DownloadService extends Service {
|
||||
startService(context, intent, foreground);
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts the service if not started already and removes all downloads.
|
||||
*
|
||||
* @param context A {@link Context}.
|
||||
* @param clazz The concrete download service to be started.
|
||||
* @param foreground Whether the service is started in the foreground.
|
||||
*/
|
||||
public static void sendRemoveAllDownloads(
|
||||
Context context, Class<? extends DownloadService> clazz, boolean foreground) {
|
||||
Intent intent = buildRemoveAllDownloadsIntent(context, clazz, foreground);
|
||||
startService(context, intent, foreground);
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts the service if not started already and resumes all downloads.
|
||||
*
|
||||
@ -560,6 +596,9 @@ public abstract class DownloadService extends Service {
|
||||
downloadManager.removeDownload(contentId);
|
||||
}
|
||||
break;
|
||||
case ACTION_REMOVE_ALL_DOWNLOADS:
|
||||
downloadManager.removeAllDownloads();
|
||||
break;
|
||||
case ACTION_RESUME_DOWNLOADS:
|
||||
downloadManager.resumeDownloads();
|
||||
break;
|
||||
|
@ -44,6 +44,13 @@ public interface WritableDownloadIndex extends DownloadIndex {
|
||||
*/
|
||||
void setDownloadingStatesToQueued() throws IOException;
|
||||
|
||||
/**
|
||||
* Sets all states to {@link Download#STATE_REMOVING}.
|
||||
*
|
||||
* @throws IOException If an error occurs updating the state.
|
||||
*/
|
||||
void setStatesToRemoving() throws IOException;
|
||||
|
||||
/**
|
||||
* Sets the stop reason of the downloads in a terminal state ({@link Download#STATE_COMPLETED},
|
||||
* {@link Download#STATE_FAILED}).
|
||||
|
@ -36,7 +36,7 @@ import com.google.android.exoplayer2.util.Util;
|
||||
* <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
|
||||
* <uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
|
||||
*
|
||||
* <service android:name="com.google.android.exoplayer2.util.scheduler.PlatformScheduler$PlatformSchedulerService"
|
||||
* <service android:name="com.google.android.exoplayer2.scheduler.PlatformScheduler$PlatformSchedulerService"
|
||||
* android:permission="android.permission.BIND_JOB_SERVICE"
|
||||
* android:exported="true"/>
|
||||
* }</pre>
|
||||
|
@ -21,7 +21,6 @@ import android.graphics.Color;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.PorterDuff;
|
||||
import android.graphics.PorterDuffXfermode;
|
||||
import android.graphics.Region;
|
||||
import android.util.SparseArray;
|
||||
import com.google.android.exoplayer2.text.Cue;
|
||||
import com.google.android.exoplayer2.util.Log;
|
||||
@ -150,6 +149,8 @@ import java.util.List;
|
||||
List<Cue> cues = new ArrayList<>();
|
||||
SparseArray<PageRegion> pageRegions = subtitleService.pageComposition.regions;
|
||||
for (int i = 0; i < pageRegions.size(); i++) {
|
||||
// Save clean clipping state.
|
||||
canvas.save();
|
||||
PageRegion pageRegion = pageRegions.valueAt(i);
|
||||
int regionId = pageRegions.keyAt(i);
|
||||
RegionComposition regionComposition = subtitleService.regions.get(regionId);
|
||||
@ -163,9 +164,7 @@ import java.util.List;
|
||||
displayDefinition.horizontalPositionMaximum);
|
||||
int clipBottom = Math.min(baseVerticalAddress + regionComposition.height,
|
||||
displayDefinition.verticalPositionMaximum);
|
||||
canvas.clipRect(baseHorizontalAddress, baseVerticalAddress, clipRight, clipBottom,
|
||||
Region.Op.REPLACE);
|
||||
|
||||
canvas.clipRect(baseHorizontalAddress, baseVerticalAddress, clipRight, clipBottom);
|
||||
ClutDefinition clutDefinition = subtitleService.cluts.get(regionComposition.clutId);
|
||||
if (clutDefinition == null) {
|
||||
clutDefinition = subtitleService.ancillaryCluts.get(regionComposition.clutId);
|
||||
@ -214,9 +213,11 @@ import java.util.List;
|
||||
(float) regionComposition.height / displayDefinition.height));
|
||||
|
||||
canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);
|
||||
// Restore clean clipping state.
|
||||
canvas.restore();
|
||||
}
|
||||
|
||||
return cues;
|
||||
return Collections.unmodifiableList(cues);
|
||||
}
|
||||
|
||||
// Static parsing.
|
||||
|
@ -89,6 +89,11 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou
|
||||
private long bytesSkipped;
|
||||
private long bytesRead;
|
||||
|
||||
/** @param userAgent The User-Agent string that should be used. */
|
||||
public DefaultHttpDataSource(String userAgent) {
|
||||
this(userAgent, /* contentTypePredicate= */ null);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param userAgent The User-Agent string that should be used.
|
||||
* @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the
|
||||
|
@ -48,6 +48,17 @@ public final class IcyDecoderTest {
|
||||
assertThat(streamInfo.url).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void decode_emptyTitle() {
|
||||
IcyDecoder decoder = new IcyDecoder();
|
||||
Metadata metadata = decoder.decode("StreamTitle='';StreamURL='test_url';");
|
||||
|
||||
assertThat(metadata.length()).isEqualTo(1);
|
||||
IcyInfo streamInfo = (IcyInfo) metadata.get(0);
|
||||
assertThat(streamInfo.title).isEmpty();
|
||||
assertThat(streamInfo.url).isEqualTo("test_url");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void decode_semiColonInTitle() {
|
||||
IcyDecoder decoder = new IcyDecoder();
|
||||
@ -70,6 +81,17 @@ public final class IcyDecoderTest {
|
||||
assertThat(streamInfo.url).isEqualTo("test_url");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void decode_lineTerminatorInTitle() {
|
||||
IcyDecoder decoder = new IcyDecoder();
|
||||
Metadata metadata = decoder.decode("StreamTitle='test\r\ntitle';StreamURL='test_url';");
|
||||
|
||||
assertThat(metadata.length()).isEqualTo(1);
|
||||
IcyInfo streamInfo = (IcyInfo) metadata.get(0);
|
||||
assertThat(streamInfo.title).isEqualTo("test\r\ntitle");
|
||||
assertThat(streamInfo.url).isEqualTo("test_url");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void decode_notIcy() {
|
||||
IcyDecoder decoder = new IcyDecoder();
|
||||
|
@ -243,6 +243,27 @@ public class DownloadManagerTest {
|
||||
downloadManagerListener.blockUntilTasksCompleteAndThrowAnyDownloadError();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void removeAllDownloads_removesAllDownloads() throws Throwable {
|
||||
// Finish one download and keep one running.
|
||||
DownloadRunner runner1 = new DownloadRunner(uri1);
|
||||
DownloadRunner runner2 = new DownloadRunner(uri2);
|
||||
runner1.postDownloadRequest();
|
||||
runner1.getDownloader(0).unblock();
|
||||
downloadManagerListener.blockUntilTasksCompleteAndThrowAnyDownloadError();
|
||||
runner2.postDownloadRequest();
|
||||
|
||||
runner1.postRemoveAllRequest();
|
||||
runner1.getDownloader(1).unblock();
|
||||
runner2.getDownloader(1).unblock();
|
||||
downloadManagerListener.blockUntilTasksCompleteAndThrowAnyDownloadError();
|
||||
|
||||
runner1.getTask().assertRemoved();
|
||||
runner2.getTask().assertRemoved();
|
||||
assertThat(downloadManager.getCurrentDownloads()).isEmpty();
|
||||
assertThat(downloadIndex.getDownloads().getCount()).isEqualTo(0);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void differentDownloadRequestsMerged() throws Throwable {
|
||||
DownloadRunner runner = new DownloadRunner(uri1);
|
||||
@ -605,6 +626,11 @@ public class DownloadManagerTest {
|
||||
return this;
|
||||
}
|
||||
|
||||
private DownloadRunner postRemoveAllRequest() {
|
||||
runOnMainThread(() -> downloadManager.removeAllDownloads());
|
||||
return this;
|
||||
}
|
||||
|
||||
private DownloadRunner postDownloadRequest(StreamKey... keys) {
|
||||
DownloadRequest downloadRequest =
|
||||
new DownloadRequest(
|
||||
|
@ -6,7 +6,9 @@ play DASH content, instantiate a `DashMediaSource` and pass it to
|
||||
|
||||
## Links ##
|
||||
|
||||
* [Developer Guide][].
|
||||
* [Javadoc][]: Classes matching `com.google.android.exoplayer2.source.dash.*`
|
||||
belong to this module.
|
||||
|
||||
[Developer Guide]: https://exoplayer.dev/dash.html
|
||||
[Javadoc]: https://exoplayer.dev/doc/reference/index.html
|
||||
|
@ -5,7 +5,9 @@ instantiate a `HlsMediaSource` and pass it to `ExoPlayer.prepare`.
|
||||
|
||||
## Links ##
|
||||
|
||||
* [Developer Guide][].
|
||||
* [Javadoc][]: Classes matching `com.google.android.exoplayer2.source.hls.*`
|
||||
belong to this module.
|
||||
|
||||
[Developer Guide]: https://exoplayer.dev/hls.html
|
||||
[Javadoc]: https://exoplayer.dev/doc/reference/index.html
|
||||
|
@ -802,7 +802,7 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper
|
||||
if (isPrimaryTrackInVariant) {
|
||||
channelCount = variantFormat.channelCount;
|
||||
selectionFlags = variantFormat.selectionFlags;
|
||||
roleFlags = mediaTagFormat.roleFlags;
|
||||
roleFlags = variantFormat.roleFlags;
|
||||
language = variantFormat.language;
|
||||
label = variantFormat.label;
|
||||
}
|
||||
|
@ -5,8 +5,10 @@ instantiate a `SsMediaSource` and pass it to `ExoPlayer.prepare`.
|
||||
|
||||
## Links ##
|
||||
|
||||
* [Developer Guide][].
|
||||
* [Javadoc][]: Classes matching
|
||||
`com.google.android.exoplayer2.source.smoothstreaming.*` belong to this
|
||||
module.
|
||||
|
||||
[Developer Guide]: https://exoplayer.dev/smoothstreaming.html
|
||||
[Javadoc]: https://exoplayer.dev/doc/reference/index.html
|
||||
|
@ -4,7 +4,9 @@ Provides UI components and resources for use with ExoPlayer.
|
||||
|
||||
## Links ##
|
||||
|
||||
* [Developer Guide][].
|
||||
* [Javadoc][]: Classes matching `com.google.android.exoplayer2.ui.*`
|
||||
belong to this module.
|
||||
|
||||
[Developer Guide]: https://exoplayer.dev/ui-components.html
|
||||
[Javadoc]: https://exoplayer.dev/doc/reference/index.html
|
||||
|
@ -27,7 +27,7 @@ import java.io.IOException;
|
||||
/** Fake {@link MediaChunk}. */
|
||||
public final class FakeMediaChunk extends MediaChunk {
|
||||
|
||||
private static final DataSource DATA_SOURCE = new DefaultHttpDataSource("TEST_AGENT", null);
|
||||
private static final DataSource DATA_SOURCE = new DefaultHttpDataSource("TEST_AGENT");
|
||||
|
||||
public FakeMediaChunk(Format trackFormat, long startTimeUs, long endTimeUs) {
|
||||
this(new DataSpec(Uri.EMPTY), trackFormat, startTimeUs, endTimeUs);
|
||||
|
Loading…
x
Reference in New Issue
Block a user