Merge pull request #5895 from google/dev-v2-r2.10.1

r2.10.1
This commit is contained in:
Oliver Woodman 2019-05-20 17:39:18 +01:00 committed by GitHub
commit 35e030f56b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
33 changed files with 365 additions and 125 deletions

View File

@ -1,5 +1,17 @@
# Release notes # # 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 ### ### 2.10.0 ###
* Core library: * Core library:

View File

@ -13,8 +13,8 @@
// limitations under the License. // limitations under the License.
project.ext { project.ext {
// ExoPlayer version and version code. // ExoPlayer version and version code.
releaseVersion = '2.10.0' releaseVersion = '2.10.1'
releaseVersionCode = 2010000 releaseVersionCode = 2010001
minSdkVersion = 16 minSdkVersion = 16
targetSdkVersion = 28 targetSdkVersion = 28
compileSdkVersion = 28 compileSdkVersion = 28

View File

@ -30,15 +30,12 @@ import com.google.android.exoplayer2.offline.DownloadIndex;
import com.google.android.exoplayer2.offline.DownloadManager; import com.google.android.exoplayer2.offline.DownloadManager;
import com.google.android.exoplayer2.offline.DownloadRequest; import com.google.android.exoplayer2.offline.DownloadRequest;
import com.google.android.exoplayer2.offline.DownloadService; 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.trackselection.MappingTrackSelector.MappedTrackInfo;
import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.util.Log; import com.google.android.exoplayer2.util.Log;
import com.google.android.exoplayer2.util.Util; import com.google.android.exoplayer2.util.Util;
import java.io.IOException; import java.io.IOException;
import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.List;
import java.util.concurrent.CopyOnWriteArraySet; import java.util.concurrent.CopyOnWriteArraySet;
/** Tracks media that has been downloaded. */ /** Tracks media that has been downloaded. */
@ -86,11 +83,9 @@ public class DownloadTracker {
} }
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
public List<StreamKey> getOfflineStreamKeys(Uri uri) { public DownloadRequest getDownloadRequest(Uri uri) {
Download download = downloads.get(uri); Download download = downloads.get(uri);
return download != null && download.state != Download.STATE_FAILED return download != null && download.state != Download.STATE_FAILED ? download.request : null;
? download.request.streamKeys
: Collections.emptyList();
} }
public void toggleDownload( public void toggleDownload(

View File

@ -45,7 +45,8 @@ import com.google.android.exoplayer2.drm.HttpMediaDrmCallback;
import com.google.android.exoplayer2.drm.UnsupportedDrmException; import com.google.android.exoplayer2.drm.UnsupportedDrmException;
import com.google.android.exoplayer2.mediacodec.MediaCodecRenderer.DecoderInitializationException; import com.google.android.exoplayer2.mediacodec.MediaCodecRenderer.DecoderInitializationException;
import com.google.android.exoplayer2.mediacodec.MediaCodecUtil.DecoderQueryException; 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.BehindLiveWindowException;
import com.google.android.exoplayer2.source.ConcatenatingMediaSource; import com.google.android.exoplayer2.source.ConcatenatingMediaSource;
import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.MediaSource;
@ -75,7 +76,6 @@ import java.lang.reflect.Constructor;
import java.net.CookieHandler; import java.net.CookieHandler;
import java.net.CookieManager; import java.net.CookieManager;
import java.net.CookiePolicy; import java.net.CookiePolicy;
import java.util.List;
import java.util.UUID; import java.util.UUID;
/** An activity that plays media using {@link SimpleExoPlayer}. */ /** An activity that plays media using {@link SimpleExoPlayer}. */
@ -457,32 +457,25 @@ public class PlayerActivity extends AppCompatActivity
} }
private MediaSource buildMediaSource(Uri uri, @Nullable String overrideExtension) { 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); @ContentType int type = Util.inferContentType(uri, overrideExtension);
List<StreamKey> offlineStreamKeys = getOfflineStreamKeys(uri);
switch (type) { switch (type) {
case C.TYPE_DASH: case C.TYPE_DASH:
return new DashMediaSource.Factory(dataSourceFactory) return new DashMediaSource.Factory(dataSourceFactory).createMediaSource(uri);
.setStreamKeys(offlineStreamKeys)
.createMediaSource(uri);
case C.TYPE_SS: case C.TYPE_SS:
return new SsMediaSource.Factory(dataSourceFactory) return new SsMediaSource.Factory(dataSourceFactory).createMediaSource(uri);
.setStreamKeys(offlineStreamKeys)
.createMediaSource(uri);
case C.TYPE_HLS: case C.TYPE_HLS:
return new HlsMediaSource.Factory(dataSourceFactory) return new HlsMediaSource.Factory(dataSourceFactory).createMediaSource(uri);
.setStreamKeys(offlineStreamKeys)
.createMediaSource(uri);
case C.TYPE_OTHER: case C.TYPE_OTHER:
return new ProgressiveMediaSource.Factory(dataSourceFactory).createMediaSource(uri); return new ProgressiveMediaSource.Factory(dataSourceFactory).createMediaSource(uri);
default: { default:
throw new IllegalStateException("Unsupported type: " + type); throw new IllegalStateException("Unsupported type: " + type);
} }
} }
}
private List<StreamKey> getOfflineStreamKeys(Uri uri) {
return ((DemoApplication) getApplication()).getDownloadTracker().getOfflineStreamKeys(uri);
}
private DefaultDrmSessionManager<FrameworkMediaCrypto> buildDrmSessionManagerV18( private DefaultDrmSessionManager<FrameworkMediaCrypto> buildDrmSessionManagerV18(
UUID uuid, String licenseUrl, String[] keyRequestPropertiesArray, boolean multiSession) UUID uuid, String licenseUrl, String[] keyRequestPropertiesArray, boolean multiSession)

View File

@ -113,7 +113,7 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
private final CronetEngine cronetEngine; private final CronetEngine cronetEngine;
private final Executor executor; private final Executor executor;
private final Predicate<String> contentTypePredicate; @Nullable private final Predicate<String> contentTypePredicate;
private final int connectTimeoutMs; private final int connectTimeoutMs;
private final int readTimeoutMs; private final int readTimeoutMs;
private final boolean resetTimeoutOnRedirects; private final boolean resetTimeoutOnRedirects;
@ -146,6 +146,18 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
private volatile long currentConnectTimeoutMs; 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 cronetEngine A CronetEngine.
* @param executor The {@link java.util.concurrent.Executor} that will handle responses. This may * @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)}. * #open(DataSpec)}.
*/ */
public CronetDataSource( public CronetDataSource(
CronetEngine cronetEngine, Executor executor, Predicate<String> contentTypePredicate) { CronetEngine cronetEngine,
Executor executor,
@Nullable Predicate<String> contentTypePredicate) {
this( this(
cronetEngine, cronetEngine,
executor, executor,
@ -188,7 +202,7 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
public CronetDataSource( public CronetDataSource(
CronetEngine cronetEngine, CronetEngine cronetEngine,
Executor executor, Executor executor,
Predicate<String> contentTypePredicate, @Nullable Predicate<String> contentTypePredicate,
int connectTimeoutMs, int connectTimeoutMs,
int readTimeoutMs, int readTimeoutMs,
boolean resetTimeoutOnRedirects, boolean resetTimeoutOnRedirects,
@ -225,7 +239,7 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
public CronetDataSource( public CronetDataSource(
CronetEngine cronetEngine, CronetEngine cronetEngine,
Executor executor, Executor executor,
Predicate<String> contentTypePredicate, @Nullable Predicate<String> contentTypePredicate,
int connectTimeoutMs, int connectTimeoutMs,
int readTimeoutMs, int readTimeoutMs,
boolean resetTimeoutOnRedirects, boolean resetTimeoutOnRedirects,
@ -246,7 +260,7 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource {
/* package */ CronetDataSource( /* package */ CronetDataSource(
CronetEngine cronetEngine, CronetEngine cronetEngine,
Executor executor, Executor executor,
Predicate<String> contentTypePredicate, @Nullable Predicate<String> contentTypePredicate,
int connectTimeoutMs, int connectTimeoutMs,
int readTimeoutMs, int readTimeoutMs,
boolean resetTimeoutOnRedirects, boolean resetTimeoutOnRedirects,

View File

@ -948,8 +948,8 @@ public final class ImaAdsLoader
@Override @Override
public void onTimelineChanged( public void onTimelineChanged(
Timeline timeline, @Nullable Object manifest, @Player.TimelineChangeReason int reason) { Timeline timeline, @Nullable Object manifest, @Player.TimelineChangeReason int reason) {
if (reason == Player.TIMELINE_CHANGE_REASON_RESET) { if (timeline.isEmpty()) {
// The player is being reset and this source will be released. // The player is being reset or contains no media.
return; return;
} }
Assertions.checkArgument(timeline.getPeriodCount() == 1); Assertions.checkArgument(timeline.getPeriodCount() == 1);

View File

@ -73,6 +73,15 @@ public class OkHttpDataSource extends BaseDataSource implements HttpDataSource {
private long bytesSkipped; private long bytesSkipped;
private long bytesRead; 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 * @param callFactory A {@link Call.Factory} (typically an {@link okhttp3.OkHttpClient}) for use
* by the source. * by the source.

View File

@ -3,3 +3,4 @@ android.useAndroidX=true
android.enableJetifier=true android.enableJetifier=true
android.enableUnitTestBinaryResources=true android.enableUnitTestBinaryResources=true
buildDir=buildout buildDir=buildout
org.gradle.jvmargs=-Xmx2g -XX:MaxMetaspaceSize=512m

View File

@ -18,10 +18,13 @@ android.libraryVariants.all { variant ->
if (!name.equals("release")) { if (!name.equals("release")) {
return; // Skip non-release builds. return; // Skip non-release builds.
} }
def allSourceDirs = variant.sourceSets.inject ([]) {
acc, val -> acc << val.javaDirectories
}
task("generateJavadoc", type: Javadoc) { task("generateJavadoc", type: Javadoc) {
description = "Generates Javadoc for the ${javadocTitle}." description = "Generates Javadoc for the ${javadocTitle}."
title = "ExoPlayer ${javadocTitle}" title = "ExoPlayer ${javadocTitle}"
source = variant.javaCompileProvider.get().source source = allSourceDirs
options { options {
links "http://docs.oracle.com/javase/7/docs/api/" links "http://docs.oracle.com/javase/7/docs/api/"
linksOffline "https://developer.android.com/reference", linksOffline "https://developer.android.com/reference",

View File

@ -46,18 +46,21 @@
# Constructors accessed via reflection in DownloadHelper # Constructors accessed via reflection in DownloadHelper
-dontnote com.google.android.exoplayer2.source.dash.DashMediaSource$Factory -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); <init>(com.google.android.exoplayer2.upstream.DataSource$Factory);
** setStreamKeys(java.util.List);
com.google.android.exoplayer2.source.dash.DashMediaSource createMediaSource(android.net.Uri); com.google.android.exoplayer2.source.dash.DashMediaSource createMediaSource(android.net.Uri);
} }
-dontnote com.google.android.exoplayer2.source.hls.HlsMediaSource$Factory -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); <init>(com.google.android.exoplayer2.upstream.DataSource$Factory);
** setStreamKeys(java.util.List);
com.google.android.exoplayer2.source.hls.HlsMediaSource createMediaSource(android.net.Uri); com.google.android.exoplayer2.source.hls.HlsMediaSource createMediaSource(android.net.Uri);
} }
-dontnote com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource$Factory -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); <init>(com.google.android.exoplayer2.upstream.DataSource$Factory);
** setStreamKeys(java.util.List);
com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource createMediaSource(android.net.Uri); com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource createMediaSource(android.net.Uri);
} }

View File

@ -1053,12 +1053,15 @@ import java.util.concurrent.atomic.AtomicBoolean;
&& nextInfo.resolvedPeriodIndex == currentPeriodIndex && nextInfo.resolvedPeriodIndex == currentPeriodIndex
&& nextInfo.resolvedPeriodTimeUs > oldPeriodPositionUs && nextInfo.resolvedPeriodTimeUs > oldPeriodPositionUs
&& nextInfo.resolvedPeriodTimeUs <= newPeriodPositionUs) { && nextInfo.resolvedPeriodTimeUs <= newPeriodPositionUs) {
try {
sendMessageToTarget(nextInfo.message); sendMessageToTarget(nextInfo.message);
} finally {
if (nextInfo.message.getDeleteAfterDelivery() || nextInfo.message.isCanceled()) { if (nextInfo.message.getDeleteAfterDelivery() || nextInfo.message.isCanceled()) {
pendingMessages.remove(nextPendingMessageIndex); pendingMessages.remove(nextPendingMessageIndex);
} else { } else {
nextPendingMessageIndex++; nextPendingMessageIndex++;
} }
}
nextInfo = nextInfo =
nextPendingMessageIndex < pendingMessages.size() nextPendingMessageIndex < pendingMessages.size()
? pendingMessages.get(nextPendingMessageIndex) ? pendingMessages.get(nextPendingMessageIndex)

View File

@ -29,11 +29,11 @@ public final class ExoPlayerLibraryInfo {
/** The version of the library expressed as a string, for example "1.2.3". */ /** 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. // 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}. */ /** The version of the library expressed as {@code "ExoPlayerLib/" + VERSION}. */
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa. // 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. * 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). * integer version 123045006 (123-045-006).
*/ */
// Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa. // 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} * Whether the library was compiled with {@link com.google.android.exoplayer2.util.Assertions}

View File

@ -1231,7 +1231,7 @@ public class SimpleExoPlayer extends BasePlayer
Log.w( Log.w(
TAG, TAG,
"Player is accessed on the wrong thread. See " "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", + "what-do-player-is-accessed-on-the-wrong-thread-warnings-mean",
hasNotifiedFullWrongThreadWarning ? null : new IllegalStateException()); hasNotifiedFullWrongThreadWarning ? null : new IllegalStateException());
hasNotifiedFullWrongThreadWarning = true; hasNotifiedFullWrongThreadWarning = true;

View File

@ -59,7 +59,7 @@ public interface AnalyticsListener {
public final Timeline timeline; 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. * if the timeline is not yet known and empty.
*/ */
public final int windowIndex; public final int windowIndex;
@ -76,7 +76,7 @@ public interface AnalyticsListener {
public final long eventPlaybackPositionMs; 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. * currently playing ad at the time of the event, in milliseconds.
*/ */
public final long currentPlaybackPositionMs; public final long currentPlaybackPositionMs;
@ -91,15 +91,15 @@ public interface AnalyticsListener {
* @param realtimeMs Elapsed real-time as returned by {@code SystemClock.elapsedRealtime()} at * @param realtimeMs Elapsed real-time as returned by {@code SystemClock.elapsedRealtime()} at
* the time of the event, in milliseconds. * the time of the event, in milliseconds.
* @param timeline Timeline at the time of the event. * @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. * 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 * @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. * {@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 * @param eventPlaybackPositionMs Position in the window or ad this event belongs to at the time
* of the event, in milliseconds. * of the event, in milliseconds.
* @param currentPlaybackPositionMs Position in the current timeline window ({@code * @param currentPlaybackPositionMs Position in the current timeline window ({@link
* timeline.getCurrentWindowIndex()} or the currently playing ad at the time of the event, * Player#getCurrentWindowIndex()}) or the currently playing ad at the time of the event, in
* in milliseconds. * milliseconds.
* @param totalBufferedDurationMs Total buffered duration from {@link * @param totalBufferedDurationMs Total buffered duration from {@link
* #currentPlaybackPositionMs} at the time of the event, in milliseconds. This includes * #currentPlaybackPositionMs} at the time of the event, in milliseconds. This includes
* pre-buffered data for subsequent ads and windows. * pre-buffered data for subsequent ads and windows.

View File

@ -786,7 +786,7 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
// Set codec configuration values. // Set codec configuration values.
if (Util.SDK_INT >= 23) { if (Util.SDK_INT >= 23) {
mediaFormat.setInteger(MediaFormat.KEY_PRIORITY, 0 /* realtime priority */); 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); 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 * Returns whether the decoder is known to output six audio channels when provided with input with
* fewer than six channels. * fewer than six channels.

View File

@ -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 * 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). * 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. */ /** The magic signature for an Opus Identification header, as defined in RFC-7845. */
private static final byte[] opusMagic = Util.getUtf8Bytes("OpusHead"); private static final byte[] opusMagic = Util.getUtf8Bytes("OpusHead");

View File

@ -31,7 +31,7 @@ public final class IcyDecoder implements MetadataDecoder {
private static final String TAG = "IcyDecoder"; 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_NAME = "streamtitle";
private static final String STREAM_KEY_URL = "streamurl"; private static final String STREAM_KEY_URL = "streamurl";

View File

@ -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 @Override
public void setStopReason(int stopReason) throws DatabaseIOException { public void setStopReason(int stopReason) throws DatabaseIOException {
ensureInitialized(); ensureInitialized();

View File

@ -20,7 +20,6 @@ import android.os.Handler;
import android.os.HandlerThread; import android.os.HandlerThread;
import android.os.Message; import android.os.Message;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import android.util.Pair;
import android.util.SparseIntArray; import android.util.SparseIntArray;
import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ExoPlaybackException; 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.MediaPeriod;
import com.google.android.exoplayer2.source.MediaSource; import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId; 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.TrackGroup;
import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.source.TrackGroupArray;
import com.google.android.exoplayer2.trackselection.BaseTrackSelection; 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.Allocator;
import com.google.android.exoplayer2.upstream.BandwidthMeter; import com.google.android.exoplayer2.upstream.BandwidthMeter;
import com.google.android.exoplayer2.upstream.DataSource; 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.DefaultAllocator;
import com.google.android.exoplayer2.upstream.TransferListener; import com.google.android.exoplayer2.upstream.TransferListener;
import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Assertions;
@ -106,30 +107,13 @@ public final class DownloadHelper {
void onPrepareError(DownloadHelper helper, IOException e); void onPrepareError(DownloadHelper helper, IOException e);
} }
@Nullable private static final Constructor<?> DASH_FACTORY_CONSTRUCTOR; private static final MediaSourceFactory DASH_FACTORY =
@Nullable private static final Constructor<?> HLS_FACTORY_CONSTRUCTOR; getMediaSourceFactory("com.google.android.exoplayer2.source.dash.DashMediaSource$Factory");
@Nullable private static final Constructor<?> SS_FACTORY_CONSTRUCTOR; private static final MediaSourceFactory SS_FACTORY =
@Nullable private static final Method DASH_FACTORY_CREATE_METHOD; getMediaSourceFactory(
@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"); "com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource$Factory");
SS_FACTORY_CONSTRUCTOR = ssFactoryMethods.first; private static final MediaSourceFactory HLS_FACTORY =
SS_FACTORY_CREATE_METHOD = ssFactoryMethods.second; getMediaSourceFactory("com.google.android.exoplayer2.source.hls.HlsMediaSource$Factory");
}
/** /**
* Creates a {@link DownloadHelper} for progressive streams. * Creates a {@link DownloadHelper} for progressive streams.
@ -202,8 +186,7 @@ public final class DownloadHelper {
DownloadRequest.TYPE_DASH, DownloadRequest.TYPE_DASH,
uri, uri,
/* cacheKey= */ null, /* cacheKey= */ null,
createMediaSource( DASH_FACTORY.createMediaSource(uri, dataSourceFactory, /* streamKeys= */ null),
uri, dataSourceFactory, DASH_FACTORY_CONSTRUCTOR, DASH_FACTORY_CREATE_METHOD),
trackSelectorParameters, trackSelectorParameters,
Util.getRendererCapabilities(renderersFactory, drmSessionManager)); Util.getRendererCapabilities(renderersFactory, drmSessionManager));
} }
@ -252,8 +235,7 @@ public final class DownloadHelper {
DownloadRequest.TYPE_HLS, DownloadRequest.TYPE_HLS,
uri, uri,
/* cacheKey= */ null, /* cacheKey= */ null,
createMediaSource( HLS_FACTORY.createMediaSource(uri, dataSourceFactory, /* streamKeys= */ null),
uri, dataSourceFactory, HLS_FACTORY_CONSTRUCTOR, HLS_FACTORY_CREATE_METHOD),
trackSelectorParameters, trackSelectorParameters,
Util.getRendererCapabilities(renderersFactory, drmSessionManager)); Util.getRendererCapabilities(renderersFactory, drmSessionManager));
} }
@ -302,11 +284,42 @@ public final class DownloadHelper {
DownloadRequest.TYPE_SS, DownloadRequest.TYPE_SS,
uri, uri,
/* cacheKey= */ null, /* cacheKey= */ null,
createMediaSource(uri, dataSourceFactory, SS_FACTORY_CONSTRUCTOR, SS_FACTORY_CREATE_METHOD), SS_FACTORY.createMediaSource(uri, dataSourceFactory, /* streamKeys= */ null),
trackSelectorParameters, trackSelectorParameters,
Util.getRendererCapabilities(renderersFactory, drmSessionManager)); 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 String downloadType;
private final Uri uri; private final Uri uri;
@Nullable private final String cacheKey; @Nullable private final String cacheKey;
@ -739,37 +752,56 @@ public final class DownloadHelper {
} }
} }
private static Pair<@NullableType Constructor<?>, @NullableType Method> private static MediaSourceFactory getMediaSourceFactory(String className) {
getMediaSourceFactoryMethods(String className) {
Constructor<?> constructor = null; Constructor<?> constructor = null;
Method setStreamKeysMethod = null;
Method createMethod = null; Method createMethod = null;
try { try {
// LINT.IfChange // LINT.IfChange
Class<?> factoryClazz = Class.forName(className); 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); createMethod = factoryClazz.getMethod("createMediaSource", Uri.class);
// LINT.ThenChange(../../../../../../../../proguard-rules.txt) // LINT.ThenChange(../../../../../../../../proguard-rules.txt)
} catch (Exception e) { } catch (ClassNotFoundException e) {
// Expected if the app was built without the respective module. // 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( private static final class MediaSourceFactory {
Uri uri, @Nullable private final Constructor<?> constructor;
DataSource.Factory dataSourceFactory, @Nullable private final Method setStreamKeysMethod;
@Nullable Constructor<?> factoryConstructor, @Nullable private final Method createMethod;
@Nullable Method createMediaSourceMethod) {
if (factoryConstructor == null || createMediaSourceMethod == null) { public MediaSourceFactory(
@Nullable Constructor<?> constructor,
@Nullable Method setStreamKeysMethod,
@Nullable Method createMethod) {
this.constructor = constructor;
this.setStreamKeysMethod = setStreamKeysMethod;
this.createMethod = createMethod;
}
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."); throw new IllegalStateException("Module missing to create media source.");
} }
try { try {
Object factory = factoryConstructor.newInstance(dataSourceFactory); Object factory = constructor.newInstance(dataSourceFactory);
return (MediaSource) Assertions.checkNotNull(createMediaSourceMethod.invoke(factory, uri)); if (streamKeys != null) {
setStreamKeysMethod.invoke(factory, streamKeys);
}
return (MediaSource) Assertions.checkNotNull(createMethod.invoke(factory, uri));
} catch (Exception e) { } catch (Exception e) {
throw new IllegalStateException("Failed to instantiate media source.", e); throw new IllegalStateException("Failed to instantiate media source.", e);
} }
} }
}
private static final class MediaPreparer private static final class MediaPreparer
implements MediaSource.SourceInfoRefreshListener, MediaPeriod.Callback, Handler.Callback { implements MediaSource.SourceInfoRefreshListener, MediaPeriod.Callback, Handler.Callback {

View File

@ -133,10 +133,11 @@ public final class DownloadManager {
private static final int MSG_SET_MIN_RETRY_COUNT = 5; private static final int MSG_SET_MIN_RETRY_COUNT = 5;
private static final int MSG_ADD_DOWNLOAD = 6; private static final int MSG_ADD_DOWNLOAD = 6;
private static final int MSG_REMOVE_DOWNLOAD = 7; private static final int MSG_REMOVE_DOWNLOAD = 7;
private static final int MSG_TASK_STOPPED = 8; private static final int MSG_REMOVE_ALL_DOWNLOADS = 8;
private static final int MSG_CONTENT_LENGTH_CHANGED = 9; private static final int MSG_TASK_STOPPED = 9;
private static final int MSG_UPDATE_PROGRESS = 10; private static final int MSG_CONTENT_LENGTH_CHANGED = 10;
private static final int MSG_RELEASE = 11; private static final int MSG_UPDATE_PROGRESS = 11;
private static final int MSG_RELEASE = 12;
private static final String TAG = "DownloadManager"; private static final String TAG = "DownloadManager";
@ -446,6 +447,12 @@ public final class DownloadManager {
internalHandler.obtainMessage(MSG_REMOVE_DOWNLOAD, id).sendToTarget(); 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 * 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. * 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; id = (String) message.obj;
removeDownload(id); removeDownload(id);
break; break;
case MSG_REMOVE_ALL_DOWNLOADS:
removeAllDownloads();
break;
case MSG_TASK_STOPPED: case MSG_TASK_STOPPED:
Task task = (Task) message.obj; Task task = (Task) message.obj;
onTaskStopped(task); onTaskStopped(task);
@ -797,6 +807,36 @@ public final class DownloadManager {
syncTasks(); 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() { private void release() {
for (Task task : activeTasks.values()) { for (Task task : activeTasks.values()) {
task.cancel(/* released= */ true); 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. // to set STATE_STOPPED either, because it doesn't have a stopReason argument.
Assertions.checkState( Assertions.checkState(
state != STATE_COMPLETED && state != STATE_FAILED && state != STATE_STOPPED); state != STATE_COMPLETED && state != STATE_FAILED && state != STATE_STOPPED);
return putDownload( return putDownload(copyDownloadWithState(download, state));
new Download(
download.request,
state,
download.startTimeMs,
/* updateTimeMs= */ System.currentTimeMillis(),
download.contentLength,
/* stopReason= */ 0,
FAILURE_REASON_NONE,
download.progress));
} }
private Download putDownload(Download download) { private Download putDownload(Download download) {
@ -1120,6 +1151,18 @@ public final class DownloadManager {
return C.INDEX_UNSET; 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) { private static int compareStartTimes(Download first, Download second) {
return Util.compareLong(first.startTimeMs, second.startTimeMs); return Util.compareLong(first.startTimeMs, second.startTimeMs);
} }

View File

@ -77,6 +77,16 @@ public abstract class DownloadService extends Service {
public static final String ACTION_REMOVE_DOWNLOAD = public static final String ACTION_REMOVE_DOWNLOAD =
"com.google.android.exoplayer.downloadService.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: * 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); .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. * Builds an {@link Intent} for resuming all downloads.
* *
@ -414,6 +437,19 @@ public abstract class DownloadService extends Service {
startService(context, intent, foreground); 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. * Starts the service if not started already and resumes all downloads.
* *
@ -560,6 +596,9 @@ public abstract class DownloadService extends Service {
downloadManager.removeDownload(contentId); downloadManager.removeDownload(contentId);
} }
break; break;
case ACTION_REMOVE_ALL_DOWNLOADS:
downloadManager.removeAllDownloads();
break;
case ACTION_RESUME_DOWNLOADS: case ACTION_RESUME_DOWNLOADS:
downloadManager.resumeDownloads(); downloadManager.resumeDownloads();
break; break;

View File

@ -44,6 +44,13 @@ public interface WritableDownloadIndex extends DownloadIndex {
*/ */
void setDownloadingStatesToQueued() throws IOException; 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}, * Sets the stop reason of the downloads in a terminal state ({@link Download#STATE_COMPLETED},
* {@link Download#STATE_FAILED}). * {@link Download#STATE_FAILED}).

View File

@ -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.RECEIVE_BOOT_COMPLETED"/>
* <uses-permission android:name="android.permission.FOREGROUND_SERVICE"/> * <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:permission="android.permission.BIND_JOB_SERVICE"
* android:exported="true"/> * android:exported="true"/>
* }</pre> * }</pre>

View File

@ -21,7 +21,6 @@ import android.graphics.Color;
import android.graphics.Paint; import android.graphics.Paint;
import android.graphics.PorterDuff; import android.graphics.PorterDuff;
import android.graphics.PorterDuffXfermode; import android.graphics.PorterDuffXfermode;
import android.graphics.Region;
import android.util.SparseArray; import android.util.SparseArray;
import com.google.android.exoplayer2.text.Cue; import com.google.android.exoplayer2.text.Cue;
import com.google.android.exoplayer2.util.Log; import com.google.android.exoplayer2.util.Log;
@ -150,6 +149,8 @@ import java.util.List;
List<Cue> cues = new ArrayList<>(); List<Cue> cues = new ArrayList<>();
SparseArray<PageRegion> pageRegions = subtitleService.pageComposition.regions; SparseArray<PageRegion> pageRegions = subtitleService.pageComposition.regions;
for (int i = 0; i < pageRegions.size(); i++) { for (int i = 0; i < pageRegions.size(); i++) {
// Save clean clipping state.
canvas.save();
PageRegion pageRegion = pageRegions.valueAt(i); PageRegion pageRegion = pageRegions.valueAt(i);
int regionId = pageRegions.keyAt(i); int regionId = pageRegions.keyAt(i);
RegionComposition regionComposition = subtitleService.regions.get(regionId); RegionComposition regionComposition = subtitleService.regions.get(regionId);
@ -163,9 +164,7 @@ import java.util.List;
displayDefinition.horizontalPositionMaximum); displayDefinition.horizontalPositionMaximum);
int clipBottom = Math.min(baseVerticalAddress + regionComposition.height, int clipBottom = Math.min(baseVerticalAddress + regionComposition.height,
displayDefinition.verticalPositionMaximum); displayDefinition.verticalPositionMaximum);
canvas.clipRect(baseHorizontalAddress, baseVerticalAddress, clipRight, clipBottom, canvas.clipRect(baseHorizontalAddress, baseVerticalAddress, clipRight, clipBottom);
Region.Op.REPLACE);
ClutDefinition clutDefinition = subtitleService.cluts.get(regionComposition.clutId); ClutDefinition clutDefinition = subtitleService.cluts.get(regionComposition.clutId);
if (clutDefinition == null) { if (clutDefinition == null) {
clutDefinition = subtitleService.ancillaryCluts.get(regionComposition.clutId); clutDefinition = subtitleService.ancillaryCluts.get(regionComposition.clutId);
@ -214,9 +213,11 @@ import java.util.List;
(float) regionComposition.height / displayDefinition.height)); (float) regionComposition.height / displayDefinition.height));
canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR); canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);
// Restore clean clipping state.
canvas.restore();
} }
return cues; return Collections.unmodifiableList(cues);
} }
// Static parsing. // Static parsing.

View File

@ -89,6 +89,11 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou
private long bytesSkipped; private long bytesSkipped;
private long bytesRead; 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 userAgent The User-Agent string that should be used.
* @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the * @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the

View File

@ -48,6 +48,17 @@ public final class IcyDecoderTest {
assertThat(streamInfo.url).isNull(); 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 @Test
public void decode_semiColonInTitle() { public void decode_semiColonInTitle() {
IcyDecoder decoder = new IcyDecoder(); IcyDecoder decoder = new IcyDecoder();
@ -70,6 +81,17 @@ public final class IcyDecoderTest {
assertThat(streamInfo.url).isEqualTo("test_url"); 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 @Test
public void decode_notIcy() { public void decode_notIcy() {
IcyDecoder decoder = new IcyDecoder(); IcyDecoder decoder = new IcyDecoder();

View File

@ -243,6 +243,27 @@ public class DownloadManagerTest {
downloadManagerListener.blockUntilTasksCompleteAndThrowAnyDownloadError(); 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 @Test
public void differentDownloadRequestsMerged() throws Throwable { public void differentDownloadRequestsMerged() throws Throwable {
DownloadRunner runner = new DownloadRunner(uri1); DownloadRunner runner = new DownloadRunner(uri1);
@ -605,6 +626,11 @@ public class DownloadManagerTest {
return this; return this;
} }
private DownloadRunner postRemoveAllRequest() {
runOnMainThread(() -> downloadManager.removeAllDownloads());
return this;
}
private DownloadRunner postDownloadRequest(StreamKey... keys) { private DownloadRunner postDownloadRequest(StreamKey... keys) {
DownloadRequest downloadRequest = DownloadRequest downloadRequest =
new DownloadRequest( new DownloadRequest(

View File

@ -6,7 +6,9 @@ play DASH content, instantiate a `DashMediaSource` and pass it to
## Links ## ## Links ##
* [Developer Guide][].
* [Javadoc][]: Classes matching `com.google.android.exoplayer2.source.dash.*` * [Javadoc][]: Classes matching `com.google.android.exoplayer2.source.dash.*`
belong to this module. belong to this module.
[Developer Guide]: https://exoplayer.dev/dash.html
[Javadoc]: https://exoplayer.dev/doc/reference/index.html [Javadoc]: https://exoplayer.dev/doc/reference/index.html

View File

@ -5,7 +5,9 @@ instantiate a `HlsMediaSource` and pass it to `ExoPlayer.prepare`.
## Links ## ## Links ##
* [Developer Guide][].
* [Javadoc][]: Classes matching `com.google.android.exoplayer2.source.hls.*` * [Javadoc][]: Classes matching `com.google.android.exoplayer2.source.hls.*`
belong to this module. belong to this module.
[Developer Guide]: https://exoplayer.dev/hls.html
[Javadoc]: https://exoplayer.dev/doc/reference/index.html [Javadoc]: https://exoplayer.dev/doc/reference/index.html

View File

@ -802,7 +802,7 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper
if (isPrimaryTrackInVariant) { if (isPrimaryTrackInVariant) {
channelCount = variantFormat.channelCount; channelCount = variantFormat.channelCount;
selectionFlags = variantFormat.selectionFlags; selectionFlags = variantFormat.selectionFlags;
roleFlags = mediaTagFormat.roleFlags; roleFlags = variantFormat.roleFlags;
language = variantFormat.language; language = variantFormat.language;
label = variantFormat.label; label = variantFormat.label;
} }

View File

@ -5,8 +5,10 @@ instantiate a `SsMediaSource` and pass it to `ExoPlayer.prepare`.
## Links ## ## Links ##
* [Developer Guide][].
* [Javadoc][]: Classes matching * [Javadoc][]: Classes matching
`com.google.android.exoplayer2.source.smoothstreaming.*` belong to this `com.google.android.exoplayer2.source.smoothstreaming.*` belong to this
module. module.
[Developer Guide]: https://exoplayer.dev/smoothstreaming.html
[Javadoc]: https://exoplayer.dev/doc/reference/index.html [Javadoc]: https://exoplayer.dev/doc/reference/index.html

View File

@ -4,7 +4,9 @@ Provides UI components and resources for use with ExoPlayer.
## Links ## ## Links ##
* [Developer Guide][].
* [Javadoc][]: Classes matching `com.google.android.exoplayer2.ui.*` * [Javadoc][]: Classes matching `com.google.android.exoplayer2.ui.*`
belong to this module. belong to this module.
[Developer Guide]: https://exoplayer.dev/ui-components.html
[Javadoc]: https://exoplayer.dev/doc/reference/index.html [Javadoc]: https://exoplayer.dev/doc/reference/index.html

View File

@ -27,7 +27,7 @@ import java.io.IOException;
/** Fake {@link MediaChunk}. */ /** Fake {@link MediaChunk}. */
public final class FakeMediaChunk extends 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) { public FakeMediaChunk(Format trackFormat, long startTimeUs, long endTimeUs) {
this(new DataSpec(Uri.EMPTY), trackFormat, startTimeUs, endTimeUs); this(new DataSpec(Uri.EMPTY), trackFormat, startTimeUs, endTimeUs);