From 79e64bc613e633ab72f37d1c760b94385d904b72 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Thu, 27 Aug 2020 12:18:53 +0100 Subject: [PATCH 001/110] Clean up release notes --- RELEASENOTES.md | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 529160f736..9ab124a188 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -1,15 +1,6 @@ # Release notes -### dev-v2 (not yet released) - -* Audio: Add an event for the audio position starting to advance, to make it - easier for apps to determine when audio playout started - ([#7577](https://github.com/google/ExoPlayer/issues/7577)). -* Extractors: - * Support Dolby Vision extraction in Matroska - ([#7267](https://github.com/google/ExoPlayer/issues/7267). - -### 2.12.0 (not yet released - targeted for 2020-08-TBD) ### +### 2.12.0 (not yet released - targeted for 2020-08-03) ### * Core library: * `Player`: @@ -113,6 +104,9 @@ ([#7502](https://github.com/google/ExoPlayer/issues/7502)). This option can be set using `DefaultRenderersFactory.setEnableAudioTrackPlaybackParams`. + * Add an event for the audio position starting to advance, to make it + easier for apps to determine when audio playout started + ([#7577](https://github.com/google/ExoPlayer/issues/7577)). * Generalize support for floating point audio. * Add an option to `DefaultAudioSink` for enabling floating point output. This option can also be set using @@ -212,6 +206,8 @@ * FMP4: Add support for partially fragmented MP4s ([#7308](https://github.com/google/ExoPlayer/issues/7308)). * Matroska: + * Support Dolby Vision extraction in Matroska + ([#7267](https://github.com/google/ExoPlayer/issues/7267). * Populate `Format.label` with track titles. * Remove support for the `Invisible` block header flag. * MPEG-TS: Add support for MPEG-4 Part 2 and H.263 From db63334f94c0e64ac4d59479d33be96fa5f33ed4 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Thu, 27 Aug 2020 12:51:11 +0100 Subject: [PATCH 002/110] Clean up release notes --- RELEASENOTES.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 9ab124a188..a7acb8ac11 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -1,6 +1,6 @@ # Release notes -### 2.12.0 (not yet released - targeted for 2020-08-03) ### +### 2.12.0 (not yet released - targeted for 2020-09-03) ### * Core library: * `Player`: @@ -206,7 +206,7 @@ * FMP4: Add support for partially fragmented MP4s ([#7308](https://github.com/google/ExoPlayer/issues/7308)). * Matroska: - * Support Dolby Vision extraction in Matroska + * Support Dolby Vision ([#7267](https://github.com/google/ExoPlayer/issues/7267). * Populate `Format.label` with track titles. * Remove support for the `Invisible` block header flag. From 6cce608f32cbce68de003e40129fe1bca97c1ab9 Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 7 Sep 2020 18:17:21 +0100 Subject: [PATCH 003/110] Simplify DefaultMediaSourceFactory ad configuration - Use a setter, which is consistent with how other optional components are passed. - Remove nesting where a provider provides another provider. Since AdSupportProvider then only provides one thing, it can be renamed to AdsLoaderProvider, which more clearly expresses what it provides. PiperOrigin-RevId: 330396334 --- .../android/exoplayer2/demo/DemoUtil.java | 11 +- .../exoplayer2/demo/PlayerActivity.java | 56 ++++---- .../demo/SampleChooserActivity.java | 5 +- .../exoplayer2/ext/ima/ImaPlaybackTest.java | 3 +- .../exoplayer2/ext/media2/PlayerTestRule.java | 2 +- .../google/android/exoplayer2/ExoPlayer.java | 2 +- .../android/exoplayer2/ExoPlayerFactory.java | 4 +- .../android/exoplayer2/MetadataRetriever.java | 6 +- .../android/exoplayer2/SimpleExoPlayer.java | 2 +- .../exoplayer2/offline/DownloadHelper.java | 2 +- .../source/DefaultMediaSourceFactory.java | 135 ++++++++---------- .../source/DefaultMediaSourceFactoryTest.java | 60 +++----- .../dash/DefaultMediaSourceFactoryTest.java | 11 +- .../hls/DefaultMediaSourceFactoryTest.java | 11 +- .../DefaultMediaSourceFactoryTest.java | 13 +- 15 files changed, 142 insertions(+), 181 deletions(-) diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/DemoUtil.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/DemoUtil.java index 669e09ed70..c3621879c5 100644 --- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/DemoUtil.java +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/DemoUtil.java @@ -50,6 +50,7 @@ public final class DemoUtil { private static final String DOWNLOAD_TRACKER_ACTION_FILE = "tracked_actions"; private static final String DOWNLOAD_CONTENT_DIRECTORY = "downloads"; + private static @MonotonicNonNull String userAgent; private static DataSource.@MonotonicNonNull Factory dataSourceFactory; private static HttpDataSource.@MonotonicNonNull Factory httpDataSourceFactory; private static @MonotonicNonNull DatabaseProvider databaseProvider; @@ -77,17 +78,23 @@ public final class DemoUtil { .setExtensionRendererMode(extensionRendererMode); } + public static synchronized String getUserAgent(Context context) { + if (userAgent == null) { + userAgent = Util.getUserAgent(context, "ExoPlayerDemo"); + } + return userAgent; + } + public static synchronized HttpDataSource.Factory getHttpDataSourceFactory(Context context) { if (httpDataSourceFactory == null) { context = context.getApplicationContext(); CronetEngineWrapper cronetEngineWrapper = new CronetEngineWrapper(context); - String userAgent = Util.getUserAgent(context, "ExoPlayerDemo"); httpDataSourceFactory = new CronetDataSourceFactory( cronetEngineWrapper, Executors.newSingleThreadExecutor(), /* transferListener= */ null, - userAgent); + getUserAgent(context)); } return httpDataSourceFactory; } diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java index 370db4ac70..c9af9f77bb 100644 --- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java @@ -47,6 +47,7 @@ import com.google.android.exoplayer2.mediacodec.MediaCodecUtil.DecoderQueryExcep import com.google.android.exoplayer2.offline.DownloadRequest; import com.google.android.exoplayer2.source.BehindLiveWindowException; import com.google.android.exoplayer2.source.DefaultMediaSourceFactory; +import com.google.android.exoplayer2.source.MediaSourceFactory; import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.source.ads.AdsLoader; import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; @@ -285,16 +286,18 @@ public class PlayerActivity extends AppCompatActivity intent.getBooleanExtra(IntentUtil.PREFER_EXTENSION_DECODERS_EXTRA, false); RenderersFactory renderersFactory = DemoUtil.buildRenderersFactory(/* context= */ this, preferExtensionDecoders); + MediaSourceFactory mediaSourceFactory = + new DefaultMediaSourceFactory(dataSourceFactory) + .setDrmUserAgent(DemoUtil.getUserAgent(this)) + .setAdsLoaderProvider(this::getAdsLoader) + .setAdViewProvider(playerView); trackSelector = new DefaultTrackSelector(/* context= */ this); trackSelector.setParameters(trackSelectorParameters); lastSeenTrackGroupArray = null; - player = new SimpleExoPlayer.Builder(/* context= */ this, renderersFactory) - .setMediaSourceFactory( - DefaultMediaSourceFactory.newInstance( - /* context= */ this, dataSourceFactory, new AdSupportProvider())) + .setMediaSourceFactory(mediaSourceFactory) .setTrackSelector(trackSelector) .build(); player.addListener(new PlayerEventListener()); @@ -361,6 +364,24 @@ public class PlayerActivity extends AppCompatActivity return mediaItems; } + private AdsLoader getAdsLoader(Uri adTagUri) { + if (mediaItems.size() > 1) { + showToast(R.string.unsupported_ads_in_playlist); + releaseAdsLoader(); + return null; + } + if (!adTagUri.equals(loadedAdTagUri)) { + releaseAdsLoader(); + loadedAdTagUri = adTagUri; + } + // The ads loader is reused for multiple playbacks, so that ad playback can resume. + if (adsLoader == null) { + adsLoader = new ImaAdsLoader(/* context= */ PlayerActivity.this, adTagUri); + } + adsLoader.setPlayer(player); + return adsLoader; + } + protected void releasePlayer() { if (player != null) { updateTrackSelectorParameters(); @@ -517,33 +538,6 @@ public class PlayerActivity extends AppCompatActivity } } - private class AdSupportProvider implements DefaultMediaSourceFactory.AdSupportProvider { - - @Override - public AdsLoader getAdsLoader(Uri adTagUri) { - if (mediaItems.size() > 1) { - showToast(R.string.unsupported_ads_in_playlist); - releaseAdsLoader(); - return null; - } - if (!adTagUri.equals(loadedAdTagUri)) { - releaseAdsLoader(); - loadedAdTagUri = adTagUri; - } - // The ads loader is reused for multiple playbacks, so that ad playback can resume. - if (adsLoader == null) { - adsLoader = new ImaAdsLoader(/* context= */ PlayerActivity.this, adTagUri); - } - adsLoader.setPlayer(player); - return adsLoader; - } - - @Override - public AdsLoader.AdViewProvider getAdViewProvider() { - return checkNotNull(playerView); - } - } - private static List createMediaItems(Intent intent, DownloadTracker downloadTracker) { List mediaItems = new ArrayList<>(); for (MediaItem item : IntentUtil.createMediaItemsFromIntent(intent)) { diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/SampleChooserActivity.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/SampleChooserActivity.java index 267b97f8ec..ea5b38ce8e 100644 --- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/SampleChooserActivity.java +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/SampleChooserActivity.java @@ -53,7 +53,6 @@ import com.google.android.exoplayer2.offline.DownloadService; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DataSourceInputStream; import com.google.android.exoplayer2.upstream.DataSpec; -import com.google.android.exoplayer2.upstream.DefaultDataSource; import com.google.android.exoplayer2.util.Log; import com.google.android.exoplayer2.util.Util; import com.google.common.collect.ImmutableList; @@ -276,9 +275,7 @@ public class SampleChooserActivity extends AppCompatActivity protected List doInBackground(String... uris) { List result = new ArrayList<>(); Context context = getApplicationContext(); - String userAgent = Util.getUserAgent(context, "ExoPlayerDemo"); - DataSource dataSource = - new DefaultDataSource(context, userAgent, /* allowCrossProtocolRedirects= */ false); + DataSource dataSource = DemoUtil.getDataSourceFactory(context).createDataSource(); for (String uri : uris) { DataSpec dataSpec = new DataSpec(Uri.parse(uri)); InputStream inputStream = new DataSourceInputStream(dataSource, dataSpec); diff --git a/extensions/ima/src/androidTest/java/com/google/android/exoplayer2/ext/ima/ImaPlaybackTest.java b/extensions/ima/src/androidTest/java/com/google/android/exoplayer2/ext/ima/ImaPlaybackTest.java index 31cd29de94..cd58e1f58b 100644 --- a/extensions/ima/src/androidTest/java/com/google/android/exoplayer2/ext/ima/ImaPlaybackTest.java +++ b/extensions/ima/src/androidTest/java/com/google/android/exoplayer2/ext/ima/ImaPlaybackTest.java @@ -243,8 +243,7 @@ public final class ImaPlaybackTest { new DefaultDataSourceFactory( context, Util.getUserAgent(context, ImaPlaybackTest.class.getSimpleName())); MediaSource contentMediaSource = - DefaultMediaSourceFactory.newInstance(context) - .createMediaSource(MediaItem.fromUri(contentUri)); + new DefaultMediaSourceFactory(context).createMediaSource(MediaItem.fromUri(contentUri)); return new AdsMediaSource( contentMediaSource, dataSourceFactory, diff --git a/extensions/media2/src/androidTest/java/com/google/android/exoplayer2/ext/media2/PlayerTestRule.java b/extensions/media2/src/androidTest/java/com/google/android/exoplayer2/ext/media2/PlayerTestRule.java index 345985f862..f5518e0c7c 100644 --- a/extensions/media2/src/androidTest/java/com/google/android/exoplayer2/ext/media2/PlayerTestRule.java +++ b/extensions/media2/src/androidTest/java/com/google/android/exoplayer2/ext/media2/PlayerTestRule.java @@ -80,7 +80,7 @@ import org.junit.rules.ExternalResource; exoPlayer = new SimpleExoPlayer.Builder(context) .setLooper(Looper.myLooper()) - .setMediaSourceFactory(new DefaultMediaSourceFactory(dataSourceFactory, null)) + .setMediaSourceFactory(new DefaultMediaSourceFactory(dataSourceFactory)) .build(); sessionPlayerConnector = new SessionPlayerConnector(exoPlayer); }); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayer.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayer.java index 5211b3eace..b5489186bc 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayer.java @@ -182,7 +182,7 @@ public interface ExoPlayer extends Player { this( renderers, new DefaultTrackSelector(context), - DefaultMediaSourceFactory.newInstance(context), + new DefaultMediaSourceFactory(context), new DefaultLoadControl(), DefaultBandwidthMeter.getSingletonInstance(context)); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerFactory.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerFactory.java index dcdce89489..dfe96ffa32 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerFactory.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerFactory.java @@ -194,7 +194,7 @@ public final class ExoPlayerFactory { context, renderersFactory, trackSelector, - DefaultMediaSourceFactory.newInstance(context), + new DefaultMediaSourceFactory(context), loadControl, bandwidthMeter, analyticsCollector, @@ -250,7 +250,7 @@ public final class ExoPlayerFactory { return new ExoPlayerImpl( renderers, trackSelector, - DefaultMediaSourceFactory.newInstance(context), + new DefaultMediaSourceFactory(context), loadControl, bandwidthMeter, /* analyticsCollector= */ null, diff --git a/library/core/src/main/java/com/google/android/exoplayer2/MetadataRetriever.java b/library/core/src/main/java/com/google/android/exoplayer2/MetadataRetriever.java index c233845e0c..72f6957865 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/MetadataRetriever.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/MetadataRetriever.java @@ -43,8 +43,8 @@ public final class MetadataRetriever { /** * Retrieves the {@link TrackGroupArray} corresponding to a {@link MediaItem}. * - *

This is equivalent to using {@code - * retrieveMetadata(DefaultMediaSourceFactory.newInstance(context), mediaItem)}. + *

This is equivalent to using {@code retrieveMetadata(new DefaultMediaSourceFactory(context), + * mediaItem)}. * * @param context The {@link Context}. * @param mediaItem The {@link MediaItem} whose metadata should be retrieved. @@ -52,7 +52,7 @@ public final class MetadataRetriever { */ public static ListenableFuture retrieveMetadata( Context context, MediaItem mediaItem) { - return retrieveMetadata(DefaultMediaSourceFactory.newInstance(context), mediaItem); + return retrieveMetadata(new DefaultMediaSourceFactory(context), mediaItem); } /** diff --git a/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java b/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java index a43973b31c..787946d6a9 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java @@ -163,7 +163,7 @@ public class SimpleExoPlayer extends BasePlayer context, renderersFactory, new DefaultTrackSelector(context), - DefaultMediaSourceFactory.newInstance(context), + new DefaultMediaSourceFactory(context), new DefaultLoadControl(), DefaultBandwidthMeter.getSingletonInstance(context), new AnalyticsCollector(Clock.DEFAULT)); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadHelper.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadHelper.java index df2d10ae53..dd868f9822 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadHelper.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadHelper.java @@ -889,7 +889,7 @@ public final class DownloadHelper { MediaItem mediaItem, DataSource.Factory dataSourceFactory, @Nullable DrmSessionManager drmSessionManager) { - return new DefaultMediaSourceFactory(dataSourceFactory, /* adSupportProvider= */ null) + return new DefaultMediaSourceFactory(dataSourceFactory) .setDrmSessionManager(drmSessionManager) .createMediaSource(mediaItem); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/DefaultMediaSourceFactory.java b/library/core/src/main/java/com/google/android/exoplayer2/source/DefaultMediaSourceFactory.java index 566f7fb1c7..89b4ffb6a5 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/DefaultMediaSourceFactory.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/DefaultMediaSourceFactory.java @@ -25,6 +25,7 @@ import com.google.android.exoplayer2.MediaItem; import com.google.android.exoplayer2.drm.DrmSessionManager; import com.google.android.exoplayer2.offline.StreamKey; import com.google.android.exoplayer2.source.ads.AdsLoader; +import com.google.android.exoplayer2.source.ads.AdsLoader.AdViewProvider; import com.google.android.exoplayer2.source.ads.AdsMediaSource; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory; @@ -68,101 +69,62 @@ import java.util.List; * the stream. * * - *

Ad support for media items with ad tag uri

+ *

Ad support for media items with ad tag URIs

* - *

For a media item with an ad tag uri, an {@link AdSupportProvider} needs to be passed to {@link - * #newInstance(Context, DataSource.Factory, AdSupportProvider)}. + *

To support media items with {@link MediaItem.PlaybackProperties#adTagUri ad tag URIs}, {@link + * #setAdsLoaderProvider} and {@link #setAdViewProvider} need to be called to configure the factory + * with the required providers. */ public final class DefaultMediaSourceFactory implements MediaSourceFactory { /** - * Provides {@link AdsLoader ads loaders} and an {@link AdsLoader.AdViewProvider} to created - * {@link AdsMediaSource AdsMediaSources}. + * Provides {@link AdsLoader} instances for media items that have {@link + * MediaItem.PlaybackProperties#adTagUri ad tag URIs}. */ - public interface AdSupportProvider { + public interface AdsLoaderProvider { /** - * Returns an {@link AdsLoader} for the given {@link Uri ad tag uri} or null if no ads loader is - * available for the given ad tag uri. + * Returns an {@link AdsLoader} for the given {@link MediaItem.PlaybackProperties#adTagUri ad + * tag URI}, or null if no ads loader is available for the given ad tag URI. * - *

This method is called for each media item for which a media source is created. + *

This method is called each time a {@link MediaSource} is created from a {@link MediaItem} + * that defines an {@link MediaItem.PlaybackProperties#adTagUri ad tag URI}. */ @Nullable AdsLoader getAdsLoader(Uri adTagUri); - - /** - * Returns an {@link AdsLoader.AdViewProvider} which is used to create {@link AdsMediaSource - * AdsMediaSources}. - */ - AdsLoader.AdViewProvider getAdViewProvider(); - } - - /** - * Creates a new instance with the given {@link Context}. - * - *

This is functionally equivalent with calling {@code #newInstance(Context, - * DefaultDataSourceFactory)}. - * - * @param context The {@link Context}. - * @return A new instance of {@link DefaultMediaSourceFactory}. - */ - public static DefaultMediaSourceFactory newInstance(Context context) { - return newInstance( - context, - new DefaultDataSourceFactory( - context, Util.getUserAgent(context, ExoPlayerLibraryInfo.VERSION_SLASHY))); - } - - /** - * Creates a new instance with the given {@link Context} and {@link DataSource.Factory}. - * - * @param context The {@link Context}. - * @param dataSourceFactory A {@link DataSource.Factory} to be used to create media sources. - * @return A new instance of {@link DefaultMediaSourceFactory}. - */ - public static DefaultMediaSourceFactory newInstance( - Context context, DataSource.Factory dataSourceFactory) { - return new DefaultMediaSourceFactory(dataSourceFactory, /* adSupportProvider= */ null) - .setDrmUserAgent(Util.getUserAgent(context, ExoPlayerLibraryInfo.VERSION_SLASHY)); - } - - /** - * Creates a new instance with the given {@link Context} and {@link DataSource.Factory}. - * - * @param context The {@link Context}. - * @param dataSourceFactory A {@link DataSource.Factory} to be used to create media sources. - * @param adSupportProvider A {@link AdSupportProvider} to be used to create ad media sources. - * @return A new instance of {@link DefaultMediaSourceFactory}. - */ - public static DefaultMediaSourceFactory newInstance( - Context context, DataSource.Factory dataSourceFactory, AdSupportProvider adSupportProvider) { - return new DefaultMediaSourceFactory(dataSourceFactory, adSupportProvider) - .setDrmUserAgent(Util.getUserAgent(context, ExoPlayerLibraryInfo.VERSION_SLASHY)); } private static final String TAG = "DefaultMediaSourceFactory"; private final MediaSourceDrmHelper mediaSourceDrmHelper; private final DataSource.Factory dataSourceFactory; - @Nullable private final AdSupportProvider adSupportProvider; private final SparseArray mediaSourceFactories; @C.ContentType private final int[] supportedTypes; + @Nullable private AdsLoaderProvider adsLoaderProvider; + @Nullable private AdViewProvider adViewProvider; @Nullable private DrmSessionManager drmSessionManager; @Nullable private List streamKeys; /** - * Creates a new instance with the {@link DataSource.Factory} for downloading media and an {@link - * AdSupportProvider} to create {@link AdsMediaSource AdsMediaSources}. + * Creates a new instance. * - * @param dataSourceFactory A {@link DataSource.Factory} to be used to create media sources. - * @param adSupportProvider An {@link AdSupportProvider} to get ads loaders and ad view providers - * to be used to create {@link AdsMediaSource AdsMediaSources}. + * @param context Any context. */ - public DefaultMediaSourceFactory( - DataSource.Factory dataSourceFactory, @Nullable AdSupportProvider adSupportProvider) { + public DefaultMediaSourceFactory(Context context) { + this( + new DefaultDataSourceFactory( + context, Util.getUserAgent(context, ExoPlayerLibraryInfo.VERSION_SLASHY))); + } + + /** + * Creates a new instance. + * + * @param dataSourceFactory A {@link DataSource.Factory} to create {@link DataSource} instances + * for requesting media data. + */ + public DefaultMediaSourceFactory(DataSource.Factory dataSourceFactory) { this.dataSourceFactory = dataSourceFactory; - this.adSupportProvider = adSupportProvider; mediaSourceDrmHelper = new MediaSourceDrmHelper(); mediaSourceFactories = loadDelegates(dataSourceFactory); supportedTypes = new int[mediaSourceFactories.size()]; @@ -171,6 +133,30 @@ public final class DefaultMediaSourceFactory implements MediaSourceFactory { } } + /** + * Sets the {@link AdsLoaderProvider} that provides {@link AdsLoader} instances for media items + * that have {@link MediaItem.PlaybackProperties#adTagUri ad tag URIs}. + * + * @param adsLoaderProvider A provider for {@link AdsLoader} instances. + * @return This factory, for convenience. + */ + public DefaultMediaSourceFactory setAdsLoaderProvider( + @Nullable AdsLoaderProvider adsLoaderProvider) { + this.adsLoaderProvider = adsLoaderProvider; + return this; + } + + /** + * Sets the {@link AdViewProvider} that provides information about views for the ad playback UI. + * + * @param adViewProvider A provider for {@link AdsLoader} instances. + * @return This factory, for convenience. + */ + public DefaultMediaSourceFactory setAdViewProvider(@Nullable AdViewProvider adViewProvider) { + this.adViewProvider = adViewProvider; + return this; + } + @Override public DefaultMediaSourceFactory setDrmHttpDataSourceFactory( @Nullable HttpDataSource.Factory drmHttpDataSourceFactory) { @@ -279,24 +265,23 @@ public final class DefaultMediaSourceFactory implements MediaSourceFactory { if (mediaItem.playbackProperties.adTagUri == null) { return mediaSource; } - if (adSupportProvider == null) { + AdsLoaderProvider adsLoaderProvider = this.adsLoaderProvider; + AdViewProvider adViewProvider = this.adViewProvider; + if (adsLoaderProvider == null || adViewProvider == null) { Log.w( TAG, - "Playing media without ads. Pass an AdsSupportProvider to the constructor for supporting" - + " media items with an ad tag uri."); + "Playing media without ads. Configure ad support by calling setAdsLoaderProvider and" + + " setAdViewProvider."); return mediaSource; } @Nullable - AdsLoader adsLoader = adSupportProvider.getAdsLoader(mediaItem.playbackProperties.adTagUri); + AdsLoader adsLoader = adsLoaderProvider.getAdsLoader(mediaItem.playbackProperties.adTagUri); if (adsLoader == null) { Log.w(TAG, "Playing media without ads. No AdsLoader for provided adTagUri"); return mediaSource; } return new AdsMediaSource( - mediaSource, - /* adMediaSourceFactory= */ this, - adsLoader, - adSupportProvider.getAdViewProvider()); + mediaSource, /* adMediaSourceFactory= */ this, adsLoader, adViewProvider); } private static SparseArray loadDelegates( diff --git a/library/core/src/test/java/com/google/android/exoplayer2/source/DefaultMediaSourceFactoryTest.java b/library/core/src/test/java/com/google/android/exoplayer2/source/DefaultMediaSourceFactoryTest.java index 8dfe73f4ad..d02f04d097 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/source/DefaultMediaSourceFactoryTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/source/DefaultMediaSourceFactoryTest.java @@ -20,14 +20,12 @@ import static org.mockito.Mockito.mock; import android.content.Context; import android.net.Uri; -import androidx.annotation.Nullable; import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.MediaItem; import com.google.android.exoplayer2.source.ads.AdsLoader; import com.google.android.exoplayer2.source.ads.AdsMediaSource; -import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory; import com.google.android.exoplayer2.util.MimeTypes; import java.util.Arrays; import java.util.Collections; @@ -45,7 +43,7 @@ public final class DefaultMediaSourceFactoryTest { @Test public void createMediaSource_fromMediaItem_returnsSameMediaItemInstance() { DefaultMediaSourceFactory defaultMediaSourceFactory = - DefaultMediaSourceFactory.newInstance(ApplicationProvider.getApplicationContext()); + new DefaultMediaSourceFactory((Context) ApplicationProvider.getApplicationContext()); MediaItem mediaItem = new MediaItem.Builder().setUri(URI_MEDIA).build(); MediaSource mediaSource = defaultMediaSourceFactory.createMediaSource(mediaItem); @@ -56,7 +54,7 @@ public final class DefaultMediaSourceFactoryTest { @Test public void createMediaSource_withoutMimeType_progressiveSource() { DefaultMediaSourceFactory defaultMediaSourceFactory = - DefaultMediaSourceFactory.newInstance(ApplicationProvider.getApplicationContext()); + new DefaultMediaSourceFactory((Context) ApplicationProvider.getApplicationContext()); MediaItem mediaItem = new MediaItem.Builder().setUri(URI_MEDIA).build(); MediaSource mediaSource = defaultMediaSourceFactory.createMediaSource(mediaItem); @@ -69,7 +67,7 @@ public final class DefaultMediaSourceFactoryTest { public void createMediaSource_withTag_tagInSource_deprecated() { Object tag = new Object(); DefaultMediaSourceFactory defaultMediaSourceFactory = - DefaultMediaSourceFactory.newInstance(ApplicationProvider.getApplicationContext()); + new DefaultMediaSourceFactory((Context) ApplicationProvider.getApplicationContext()); MediaItem mediaItem = new MediaItem.Builder().setUri(URI_MEDIA).setTag(tag).build(); MediaSource mediaSource = defaultMediaSourceFactory.createMediaSource(mediaItem); @@ -80,7 +78,7 @@ public final class DefaultMediaSourceFactoryTest { @Test public void createMediaSource_withPath_progressiveSource() { DefaultMediaSourceFactory defaultMediaSourceFactory = - DefaultMediaSourceFactory.newInstance(ApplicationProvider.getApplicationContext()); + new DefaultMediaSourceFactory((Context) ApplicationProvider.getApplicationContext()); MediaItem mediaItem = new MediaItem.Builder().setUri(URI_MEDIA + "/file.mp3").build(); MediaSource mediaSource = defaultMediaSourceFactory.createMediaSource(mediaItem); @@ -91,7 +89,7 @@ public final class DefaultMediaSourceFactoryTest { @Test public void createMediaSource_withNull_usesNonNullDefaults() { DefaultMediaSourceFactory defaultMediaSourceFactory = - DefaultMediaSourceFactory.newInstance(ApplicationProvider.getApplicationContext()); + new DefaultMediaSourceFactory((Context) ApplicationProvider.getApplicationContext()); MediaItem mediaItem = new MediaItem.Builder().setUri(URI_MEDIA).build(); MediaSource mediaSource = @@ -107,7 +105,7 @@ public final class DefaultMediaSourceFactoryTest { @Test public void createMediaSource_withSubtitle_isMergingMediaSource() { DefaultMediaSourceFactory defaultMediaSourceFactory = - DefaultMediaSourceFactory.newInstance(ApplicationProvider.getApplicationContext()); + new DefaultMediaSourceFactory((Context) ApplicationProvider.getApplicationContext()); List subtitles = Arrays.asList( new MediaItem.Subtitle(Uri.parse(URI_TEXT), MimeTypes.APPLICATION_TTML, "en"), @@ -124,7 +122,7 @@ public final class DefaultMediaSourceFactoryTest { @SuppressWarnings("deprecation") // Testing deprecated MediaSource.getTag() still works. public void createMediaSource_withSubtitle_hasTag_deprecated() { DefaultMediaSourceFactory defaultMediaSourceFactory = - DefaultMediaSourceFactory.newInstance(ApplicationProvider.getApplicationContext()); + new DefaultMediaSourceFactory((Context) ApplicationProvider.getApplicationContext()); Object tag = new Object(); MediaItem mediaItem = new MediaItem.Builder() @@ -143,7 +141,7 @@ public final class DefaultMediaSourceFactoryTest { @Test public void createMediaSource_withStartPosition_isClippingMediaSource() { DefaultMediaSourceFactory defaultMediaSourceFactory = - DefaultMediaSourceFactory.newInstance(ApplicationProvider.getApplicationContext()); + new DefaultMediaSourceFactory((Context) ApplicationProvider.getApplicationContext()); MediaItem mediaItem = new MediaItem.Builder().setUri(URI_MEDIA).setClipStartPositionMs(1000L).build(); @@ -155,7 +153,7 @@ public final class DefaultMediaSourceFactoryTest { @Test public void createMediaSource_withEndPosition_isClippingMediaSource() { DefaultMediaSourceFactory defaultMediaSourceFactory = - DefaultMediaSourceFactory.newInstance(ApplicationProvider.getApplicationContext()); + new DefaultMediaSourceFactory((Context) ApplicationProvider.getApplicationContext()); MediaItem mediaItem = new MediaItem.Builder().setUri(URI_MEDIA).setClipEndPositionMs(1000L).build(); @@ -167,7 +165,7 @@ public final class DefaultMediaSourceFactoryTest { @Test public void createMediaSource_relativeToDefaultPosition_isClippingMediaSource() { DefaultMediaSourceFactory defaultMediaSourceFactory = - DefaultMediaSourceFactory.newInstance(ApplicationProvider.getApplicationContext()); + new DefaultMediaSourceFactory((Context) ApplicationProvider.getApplicationContext()); MediaItem mediaItem = new MediaItem.Builder().setUri(URI_MEDIA).setClipRelativeToDefaultPosition(true).build(); @@ -179,7 +177,7 @@ public final class DefaultMediaSourceFactoryTest { @Test public void createMediaSource_defaultToEnd_isNotClippingMediaSource() { DefaultMediaSourceFactory defaultMediaSourceFactory = - DefaultMediaSourceFactory.newInstance(ApplicationProvider.getApplicationContext()); + new DefaultMediaSourceFactory((Context) ApplicationProvider.getApplicationContext()); MediaItem mediaItem = new MediaItem.Builder() .setUri(URI_MEDIA) @@ -194,7 +192,7 @@ public final class DefaultMediaSourceFactoryTest { @Test public void getSupportedTypes_coreModule_onlyOther() { int[] supportedTypes = - DefaultMediaSourceFactory.newInstance(ApplicationProvider.getApplicationContext()) + new DefaultMediaSourceFactory((Context) ApplicationProvider.getApplicationContext()) .getSupportedTypes(); assertThat(supportedTypes).asList().containsExactly(C.TYPE_OTHER); @@ -202,14 +200,12 @@ public final class DefaultMediaSourceFactoryTest { @Test public void createMediaSource_withAdTagUri_callsAdsLoader() { - Context applicationContext = ApplicationProvider.getApplicationContext(); Uri adTagUri = Uri.parse(URI_MEDIA); MediaItem mediaItem = new MediaItem.Builder().setUri(URI_MEDIA).setAdTagUri(adTagUri).build(); DefaultMediaSourceFactory defaultMediaSourceFactory = - DefaultMediaSourceFactory.newInstance( - applicationContext, - new DefaultDataSourceFactory(applicationContext, /* userAgent= */ "ua"), - createAdSupportProvider(mock(AdsLoader.class), mock(AdsLoader.AdViewProvider.class))); + new DefaultMediaSourceFactory((Context) ApplicationProvider.getApplicationContext()) + .setAdsLoaderProvider(ignoredAdTagUri -> mock(AdsLoader.class)) + .setAdViewProvider(mock(AdsLoader.AdViewProvider.class)); MediaSource mediaSource = defaultMediaSourceFactory.createMediaSource(mediaItem); @@ -217,15 +213,11 @@ public final class DefaultMediaSourceFactoryTest { } @Test - public void createMediaSource_withAdTagUriAdsLoaderNull_playsWithoutAdNoException() { - Context applicationContext = ApplicationProvider.getApplicationContext(); + public void createMediaSource_withAdTagUri_adProvidersNotSet_playsWithoutAdNoException() { MediaItem mediaItem = new MediaItem.Builder().setUri(URI_MEDIA).setAdTagUri(Uri.parse(URI_MEDIA)).build(); DefaultMediaSourceFactory defaultMediaSourceFactory = - DefaultMediaSourceFactory.newInstance( - applicationContext, - new DefaultDataSourceFactory(applicationContext, /* userAgent= */ "ua"), - createAdSupportProvider(/* adsLoader= */ null, mock(AdsLoader.AdViewProvider.class))); + new DefaultMediaSourceFactory((Context) ApplicationProvider.getApplicationContext()); MediaSource mediaSource = defaultMediaSourceFactory.createMediaSource(mediaItem); @@ -239,24 +231,8 @@ public final class DefaultMediaSourceFactoryTest { new MediaItem.Builder().setUri(URI_MEDIA).setAdTagUri(Uri.parse(URI_MEDIA)).build(); MediaSource mediaSource = - DefaultMediaSourceFactory.newInstance(applicationContext).createMediaSource(mediaItem); + new DefaultMediaSourceFactory(applicationContext).createMediaSource(mediaItem); assertThat(mediaSource).isNotInstanceOf(AdsMediaSource.class); } - - private static DefaultMediaSourceFactory.AdSupportProvider createAdSupportProvider( - @Nullable AdsLoader adsLoader, AdsLoader.AdViewProvider adViewProvider) { - return new DefaultMediaSourceFactory.AdSupportProvider() { - @Nullable - @Override - public AdsLoader getAdsLoader(Uri adTagUri) { - return adsLoader; - } - - @Override - public AdsLoader.AdViewProvider getAdViewProvider() { - return adViewProvider; - } - }; - } } diff --git a/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/DefaultMediaSourceFactoryTest.java b/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/DefaultMediaSourceFactoryTest.java index 4ed34b0164..ab7f456c55 100644 --- a/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/DefaultMediaSourceFactoryTest.java +++ b/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/DefaultMediaSourceFactoryTest.java @@ -17,6 +17,7 @@ package com.google.android.exoplayer2.source.dash; import static com.google.common.truth.Truth.assertThat; +import android.content.Context; import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.android.exoplayer2.C; @@ -36,7 +37,7 @@ public class DefaultMediaSourceFactoryTest { @Test public void createMediaSource_withMimeType_dashSource() { DefaultMediaSourceFactory defaultMediaSourceFactory = - DefaultMediaSourceFactory.newInstance(ApplicationProvider.getApplicationContext()); + new DefaultMediaSourceFactory((Context) ApplicationProvider.getApplicationContext()); MediaItem mediaItem = new MediaItem.Builder().setUri(URI_MEDIA).setMimeType(MimeTypes.APPLICATION_MPD).build(); @@ -49,7 +50,7 @@ public class DefaultMediaSourceFactoryTest { public void createMediaSource_withTag_tagInSource() { Object tag = new Object(); DefaultMediaSourceFactory defaultMediaSourceFactory = - DefaultMediaSourceFactory.newInstance(ApplicationProvider.getApplicationContext()); + new DefaultMediaSourceFactory((Context) ApplicationProvider.getApplicationContext()); MediaItem mediaItem = new MediaItem.Builder() .setUri(URI_MEDIA) @@ -65,7 +66,7 @@ public class DefaultMediaSourceFactoryTest { @Test public void createMediaSource_withPath_dashSource() { DefaultMediaSourceFactory defaultMediaSourceFactory = - DefaultMediaSourceFactory.newInstance(ApplicationProvider.getApplicationContext()); + new DefaultMediaSourceFactory((Context) ApplicationProvider.getApplicationContext()); MediaItem mediaItem = new MediaItem.Builder().setUri(URI_MEDIA + "/file.mpd").build(); MediaSource mediaSource = defaultMediaSourceFactory.createMediaSource(mediaItem); @@ -76,7 +77,7 @@ public class DefaultMediaSourceFactoryTest { @Test public void createMediaSource_withNull_usesNonNullDefaults() { DefaultMediaSourceFactory defaultMediaSourceFactory = - DefaultMediaSourceFactory.newInstance(ApplicationProvider.getApplicationContext()); + new DefaultMediaSourceFactory((Context) ApplicationProvider.getApplicationContext()); MediaItem mediaItem = new MediaItem.Builder().setUri(URI_MEDIA + "/file.mpd").build(); MediaSource mediaSource = @@ -92,7 +93,7 @@ public class DefaultMediaSourceFactoryTest { @Test public void getSupportedTypes_dashModule_containsTypeDash() { int[] supportedTypes = - DefaultMediaSourceFactory.newInstance(ApplicationProvider.getApplicationContext()) + new DefaultMediaSourceFactory((Context) ApplicationProvider.getApplicationContext()) .getSupportedTypes(); assertThat(supportedTypes).asList().containsExactly(C.TYPE_OTHER, C.TYPE_DASH); diff --git a/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/DefaultMediaSourceFactoryTest.java b/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/DefaultMediaSourceFactoryTest.java index d46da26ff2..54383ffe33 100644 --- a/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/DefaultMediaSourceFactoryTest.java +++ b/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/DefaultMediaSourceFactoryTest.java @@ -17,6 +17,7 @@ package com.google.android.exoplayer2.source.hls; import static com.google.common.truth.Truth.assertThat; +import android.content.Context; import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.android.exoplayer2.C; @@ -36,7 +37,7 @@ public class DefaultMediaSourceFactoryTest { @Test public void createMediaSource_withMimeType_hlsSource() { DefaultMediaSourceFactory defaultMediaSourceFactory = - DefaultMediaSourceFactory.newInstance(ApplicationProvider.getApplicationContext()); + new DefaultMediaSourceFactory((Context) ApplicationProvider.getApplicationContext()); MediaItem mediaItem = new MediaItem.Builder().setUri(URI_MEDIA).setMimeType(MimeTypes.APPLICATION_M3U8).build(); @@ -49,7 +50,7 @@ public class DefaultMediaSourceFactoryTest { public void createMediaSource_withTag_tagInSource() { Object tag = new Object(); DefaultMediaSourceFactory defaultMediaSourceFactory = - DefaultMediaSourceFactory.newInstance(ApplicationProvider.getApplicationContext()); + new DefaultMediaSourceFactory((Context) ApplicationProvider.getApplicationContext()); MediaItem mediaItem = new MediaItem.Builder() .setUri(URI_MEDIA) @@ -65,7 +66,7 @@ public class DefaultMediaSourceFactoryTest { @Test public void createMediaSource_withPath_hlsSource() { DefaultMediaSourceFactory defaultMediaSourceFactory = - DefaultMediaSourceFactory.newInstance(ApplicationProvider.getApplicationContext()); + new DefaultMediaSourceFactory((Context) ApplicationProvider.getApplicationContext()); MediaItem mediaItem = new MediaItem.Builder().setUri(URI_MEDIA + "/file.m3u8").build(); MediaSource mediaSource = defaultMediaSourceFactory.createMediaSource(mediaItem); @@ -76,7 +77,7 @@ public class DefaultMediaSourceFactoryTest { @Test public void createMediaSource_withNull_usesNonNullDefaults() { DefaultMediaSourceFactory defaultMediaSourceFactory = - DefaultMediaSourceFactory.newInstance(ApplicationProvider.getApplicationContext()); + new DefaultMediaSourceFactory((Context) ApplicationProvider.getApplicationContext()); MediaItem mediaItem = new MediaItem.Builder().setUri(URI_MEDIA + "/file.m3u8").build(); MediaSource mediaSource = @@ -92,7 +93,7 @@ public class DefaultMediaSourceFactoryTest { @Test public void getSupportedTypes_hlsModule_containsTypeHls() { int[] supportedTypes = - DefaultMediaSourceFactory.newInstance(ApplicationProvider.getApplicationContext()) + new DefaultMediaSourceFactory((Context) ApplicationProvider.getApplicationContext()) .getSupportedTypes(); assertThat(supportedTypes).asList().containsExactly(C.TYPE_OTHER, C.TYPE_HLS); diff --git a/library/smoothstreaming/src/test/java/com/google/android/exoplayer2/source/smoothstreaming/DefaultMediaSourceFactoryTest.java b/library/smoothstreaming/src/test/java/com/google/android/exoplayer2/source/smoothstreaming/DefaultMediaSourceFactoryTest.java index 016acdbf3d..43c62071d3 100644 --- a/library/smoothstreaming/src/test/java/com/google/android/exoplayer2/source/smoothstreaming/DefaultMediaSourceFactoryTest.java +++ b/library/smoothstreaming/src/test/java/com/google/android/exoplayer2/source/smoothstreaming/DefaultMediaSourceFactoryTest.java @@ -17,6 +17,7 @@ package com.google.android.exoplayer2.source.smoothstreaming; import static com.google.common.truth.Truth.assertThat; +import android.content.Context; import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.android.exoplayer2.C; @@ -38,7 +39,7 @@ public class DefaultMediaSourceFactoryTest { @Test public void createMediaSource_withMimeType_smoothstreamingSource() { DefaultMediaSourceFactory defaultMediaSourceFactory = - DefaultMediaSourceFactory.newInstance(ApplicationProvider.getApplicationContext()); + new DefaultMediaSourceFactory((Context) ApplicationProvider.getApplicationContext()); MediaItem mediaItem = new MediaItem.Builder().setUri(URI_MEDIA).setMimeType(MimeTypes.APPLICATION_SS).build(); MediaSource mediaSource = defaultMediaSourceFactory.createMediaSource(mediaItem); @@ -49,7 +50,7 @@ public class DefaultMediaSourceFactoryTest { public void createMediaSource_withTag_tagInSource() { Object tag = new Object(); DefaultMediaSourceFactory defaultMediaSourceFactory = - DefaultMediaSourceFactory.newInstance(ApplicationProvider.getApplicationContext()); + new DefaultMediaSourceFactory((Context) ApplicationProvider.getApplicationContext()); MediaItem mediaItem = new MediaItem.Builder() .setUri(URI_MEDIA) @@ -65,7 +66,7 @@ public class DefaultMediaSourceFactoryTest { @Test public void createMediaSource_withIsmPath_smoothstreamingSource() { DefaultMediaSourceFactory defaultMediaSourceFactory = - DefaultMediaSourceFactory.newInstance(ApplicationProvider.getApplicationContext()); + new DefaultMediaSourceFactory((Context) ApplicationProvider.getApplicationContext()); MediaItem mediaItem = new MediaItem.Builder().setUri(URI_MEDIA + "/file.ism").build(); MediaSource mediaSource = defaultMediaSourceFactory.createMediaSource(mediaItem); @@ -76,7 +77,7 @@ public class DefaultMediaSourceFactoryTest { @Test public void createMediaSource_withManifestPath_smoothstreamingSource() { DefaultMediaSourceFactory defaultMediaSourceFactory = - DefaultMediaSourceFactory.newInstance(ApplicationProvider.getApplicationContext()); + new DefaultMediaSourceFactory((Context) ApplicationProvider.getApplicationContext()); MediaItem mediaItem = new MediaItem.Builder().setUri(URI_MEDIA + ".ism/Manifest").build(); MediaSource mediaSource = defaultMediaSourceFactory.createMediaSource(mediaItem); @@ -87,7 +88,7 @@ public class DefaultMediaSourceFactoryTest { @Test public void createMediaSource_withNull_usesNonNullDefaults() { DefaultMediaSourceFactory defaultMediaSourceFactory = - DefaultMediaSourceFactory.newInstance(ApplicationProvider.getApplicationContext()); + new DefaultMediaSourceFactory((Context) ApplicationProvider.getApplicationContext()); MediaItem mediaItem = new MediaItem.Builder().setUri(URI_MEDIA + "/file.ism").build(); MediaSource mediaSource = @@ -103,7 +104,7 @@ public class DefaultMediaSourceFactoryTest { @Test public void getSupportedTypes_smoothstreamingModule_containsTypeSS() { int[] supportedTypes = - DefaultMediaSourceFactory.newInstance(ApplicationProvider.getApplicationContext()) + new DefaultMediaSourceFactory((Context) ApplicationProvider.getApplicationContext()) .getSupportedTypes(); assertThat(supportedTypes).asList().containsExactly(C.TYPE_OTHER, C.TYPE_SS); From 6c7884436abfc2ca2d72eb0634933204bba18ec6 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Mon, 7 Sep 2020 20:48:28 +0100 Subject: [PATCH 004/110] Remove unfinished FfmpegVideoRenderer from release --- .../exoplayer2/ext/ffmpeg/FfmpegLibrary.java | 2 +- .../ext/ffmpeg/FfmpegVideoRenderer.java | 122 ------------------ library/core/proguard-rules.txt | 4 - .../exoplayer2/DefaultRenderersFactory.java | 28 ---- 4 files changed, 1 insertion(+), 155 deletions(-) delete mode 100644 extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegVideoRenderer.java diff --git a/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegLibrary.java b/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegLibrary.java index 71912aea2f..a9b862b2b2 100644 --- a/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegLibrary.java +++ b/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegLibrary.java @@ -45,7 +45,7 @@ public final class FfmpegLibrary { /** * Override the names of the FFmpeg native libraries. If an application wishes to call this * method, it must do so before calling any other method defined by this class, and before - * instantiating a {@link FfmpegAudioRenderer} or {@link FfmpegVideoRenderer} instance. + * instantiating a {@link FfmpegAudioRenderer} instance. * * @param libraries The names of the FFmpeg native libraries. */ diff --git a/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegVideoRenderer.java b/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegVideoRenderer.java deleted file mode 100644 index d2f2fce639..0000000000 --- a/extensions/ffmpeg/src/main/java/com/google/android/exoplayer2/ext/ffmpeg/FfmpegVideoRenderer.java +++ /dev/null @@ -1,122 +0,0 @@ -/* - * Copyright (C) 2020 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.google.android.exoplayer2.ext.ffmpeg; - -import android.os.Handler; -import android.view.Surface; -import androidx.annotation.Nullable; -import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.Format; -import com.google.android.exoplayer2.RendererCapabilities; -import com.google.android.exoplayer2.decoder.Decoder; -import com.google.android.exoplayer2.drm.ExoMediaCrypto; -import com.google.android.exoplayer2.util.TraceUtil; -import com.google.android.exoplayer2.util.Util; -import com.google.android.exoplayer2.video.DecoderVideoRenderer; -import com.google.android.exoplayer2.video.VideoDecoderInputBuffer; -import com.google.android.exoplayer2.video.VideoDecoderOutputBuffer; -import com.google.android.exoplayer2.video.VideoRendererEventListener; - -// TODO: Remove the NOTE below. -/** - * NOTE: This class if under development and is not yet functional. - * - *

Decodes and renders video using FFmpeg. - */ -public final class FfmpegVideoRenderer extends DecoderVideoRenderer { - - private static final String TAG = "FfmpegVideoRenderer"; - - /** - * Creates a new instance. - * - * @param allowedJoiningTimeMs The maximum duration in milliseconds for which this video renderer - * can attempt to seamlessly join an ongoing playback. - * @param eventHandler A handler to use when delivering events to {@code eventListener}. May be - * null if delivery of events is not required. - * @param eventListener A listener of events. May be null if delivery of events is not required. - * @param maxDroppedFramesToNotify The maximum number of frames that can be dropped between - * invocations of {@link VideoRendererEventListener#onDroppedFrames(int, long)}. - */ - public FfmpegVideoRenderer( - long allowedJoiningTimeMs, - @Nullable Handler eventHandler, - @Nullable VideoRendererEventListener eventListener, - int maxDroppedFramesToNotify) { - super(allowedJoiningTimeMs, eventHandler, eventListener, maxDroppedFramesToNotify); - // TODO: Implement. - } - - @Override - public String getName() { - return TAG; - } - - @Override - @RendererCapabilities.Capabilities - public final int supportsFormat(Format format) { - // TODO: Remove this line and uncomment the implementation below. - return FORMAT_UNSUPPORTED_TYPE; - /* - String mimeType = Assertions.checkNotNull(format.sampleMimeType); - if (!FfmpegLibrary.isAvailable() || !MimeTypes.isVideo(mimeType)) { - return FORMAT_UNSUPPORTED_TYPE; - } else if (!FfmpegLibrary.supportsFormat(format.sampleMimeType)) { - return RendererCapabilities.create(FORMAT_UNSUPPORTED_SUBTYPE); - } else if (format.exoMediaCryptoType != null) { - return RendererCapabilities.create(FORMAT_UNSUPPORTED_DRM); - } else { - return RendererCapabilities.create( - FORMAT_HANDLED, - ADAPTIVE_SEAMLESS, - TUNNELING_NOT_SUPPORTED); - } - */ - } - - @SuppressWarnings("return.type.incompatible") - @Override - protected Decoder - createDecoder(Format format, @Nullable ExoMediaCrypto mediaCrypto) - throws FfmpegDecoderException { - TraceUtil.beginSection("createFfmpegVideoDecoder"); - // TODO: Implement, remove the SuppressWarnings annotation, and update the return type to use - // the concrete type of the decoder (probably FfmepgVideoDecoder). - TraceUtil.endSection(); - return null; - } - - @Override - protected void renderOutputBufferToSurface(VideoDecoderOutputBuffer outputBuffer, Surface surface) - throws FfmpegDecoderException { - // TODO: Implement. - } - - @Override - protected void setDecoderOutputMode(@C.VideoOutputMode int outputMode) { - // TODO: Uncomment the implementation below. - /* - if (decoder != null) { - decoder.setOutputMode(outputMode); - } - */ - } - - @Override - protected boolean canKeepCodec(Format oldFormat, Format newFormat) { - return Util.areEqual(oldFormat.sampleMimeType, newFormat.sampleMimeType); - } -} diff --git a/library/core/proguard-rules.txt b/library/core/proguard-rules.txt index c599a93e5a..64c8cb2435 100644 --- a/library/core/proguard-rules.txt +++ b/library/core/proguard-rules.txt @@ -25,10 +25,6 @@ -keepclassmembers class com.google.android.exoplayer2.ext.av1.Libgav1VideoRenderer { (long, android.os.Handler, com.google.android.exoplayer2.video.VideoRendererEventListener, int); } --dontnote com.google.android.exoplayer2.ext.ffmpeg.FfmpegVideoRenderer --keepclassmembers class com.google.android.exoplayer2.ext.ffmpeg.FfmpegVideoRenderer { - (long, android.os.Handler, com.google.android.exoplayer2.video.VideoRendererEventListener, int); -} -dontnote com.google.android.exoplayer2.ext.opus.LibopusAudioRenderer -keepclassmembers class com.google.android.exoplayer2.ext.opus.LibopusAudioRenderer { (android.os.Handler, com.google.android.exoplayer2.audio.AudioRendererEventListener, com.google.android.exoplayer2.audio.AudioSink); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/DefaultRenderersFactory.java b/library/core/src/main/java/com/google/android/exoplayer2/DefaultRenderersFactory.java index 3558a319ba..b00cafece2 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/DefaultRenderersFactory.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/DefaultRenderersFactory.java @@ -436,34 +436,6 @@ public class DefaultRenderersFactory implements RenderersFactory { // The extension is present, but instantiation failed. throw new RuntimeException("Error instantiating AV1 extension", e); } - - try { - // Full class names used for constructor args so the LINT rule triggers if any of them move. - // LINT.IfChange - Class clazz = - Class.forName("com.google.android.exoplayer2.ext.ffmpeg.FfmpegVideoRenderer"); - Constructor constructor = - clazz.getConstructor( - long.class, - android.os.Handler.class, - com.google.android.exoplayer2.video.VideoRendererEventListener.class, - int.class); - // LINT.ThenChange(../../../../../../../proguard-rules.txt) - Renderer renderer = - (Renderer) - constructor.newInstance( - allowedVideoJoiningTimeMs, - eventHandler, - eventListener, - MAX_DROPPED_VIDEO_FRAME_COUNT_TO_NOTIFY); - out.add(extensionRendererIndex++, renderer); - Log.i(TAG, "Loaded FfmpegVideoRenderer."); - } catch (ClassNotFoundException e) { - // Expected if the app was built without the extension. - } catch (Exception e) { - // The extension is present, but instantiation failed. - throw new RuntimeException("Error instantiating FFmpeg extension", e); - } } /** From 7dd4c736c3daa283ca829f9070ce30329524f613 Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 7 Sep 2020 21:17:29 +0100 Subject: [PATCH 005/110] Fix extension renderer test names PiperOrigin-RevId: 330409635 --- .../exoplayer2/ext/ffmpeg/DefaultRenderersFactoryTest.java | 6 ++++-- .../exoplayer2/ext/flac/DefaultRenderersFactoryTest.java | 2 +- .../exoplayer2/ext/opus/DefaultRenderersFactoryTest.java | 2 +- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/extensions/ffmpeg/src/test/java/com/google/android/exoplayer2/ext/ffmpeg/DefaultRenderersFactoryTest.java b/extensions/ffmpeg/src/test/java/com/google/android/exoplayer2/ext/ffmpeg/DefaultRenderersFactoryTest.java index a52d1b1d7a..fa4c6809aa 100644 --- a/extensions/ffmpeg/src/test/java/com/google/android/exoplayer2/ext/ffmpeg/DefaultRenderersFactoryTest.java +++ b/extensions/ffmpeg/src/test/java/com/google/android/exoplayer2/ext/ffmpeg/DefaultRenderersFactoryTest.java @@ -21,12 +21,14 @@ import com.google.android.exoplayer2.testutil.DefaultRenderersFactoryAsserts; import org.junit.Test; import org.junit.runner.RunWith; -/** Unit test for {@link DefaultRenderersFactoryTest} with {@link FfmpegAudioRenderer}. */ +/** + * Unit test for {@link DefaultRenderersFactoryTest} with {@link FfmpegAudioRenderer}. + */ @RunWith(AndroidJUnit4.class) public final class DefaultRenderersFactoryTest { @Test - public void createRenderers_instantiatesVpxRenderer() { + public void createRenderers_instantiatesFfmpegAudioRenderer() { DefaultRenderersFactoryAsserts.assertExtensionRendererCreated( FfmpegAudioRenderer.class, C.TRACK_TYPE_AUDIO); } diff --git a/extensions/flac/src/test/java/com/google/android/exoplayer2/ext/flac/DefaultRenderersFactoryTest.java b/extensions/flac/src/test/java/com/google/android/exoplayer2/ext/flac/DefaultRenderersFactoryTest.java index fb20ff1114..3fb8f2cece 100644 --- a/extensions/flac/src/test/java/com/google/android/exoplayer2/ext/flac/DefaultRenderersFactoryTest.java +++ b/extensions/flac/src/test/java/com/google/android/exoplayer2/ext/flac/DefaultRenderersFactoryTest.java @@ -26,7 +26,7 @@ import org.junit.runner.RunWith; public final class DefaultRenderersFactoryTest { @Test - public void createRenderers_instantiatesVpxRenderer() { + public void createRenderers_instantiatesFlacRenderer() { DefaultRenderersFactoryAsserts.assertExtensionRendererCreated( LibflacAudioRenderer.class, C.TRACK_TYPE_AUDIO); } diff --git a/extensions/opus/src/test/java/com/google/android/exoplayer2/ext/opus/DefaultRenderersFactoryTest.java b/extensions/opus/src/test/java/com/google/android/exoplayer2/ext/opus/DefaultRenderersFactoryTest.java index e57ad84a41..9931f2d05f 100644 --- a/extensions/opus/src/test/java/com/google/android/exoplayer2/ext/opus/DefaultRenderersFactoryTest.java +++ b/extensions/opus/src/test/java/com/google/android/exoplayer2/ext/opus/DefaultRenderersFactoryTest.java @@ -26,7 +26,7 @@ import org.junit.runner.RunWith; public final class DefaultRenderersFactoryTest { @Test - public void createRenderers_instantiatesVpxRenderer() { + public void createRenderers_instantiatesOpusRenderer() { DefaultRenderersFactoryAsserts.assertExtensionRendererCreated( LibopusAudioRenderer.class, C.TRACK_TYPE_AUDIO); } From 6290d093c19cd3145f14cf6025379918677859a5 Mon Sep 17 00:00:00 2001 From: tonihei Date: Tue, 8 Sep 2020 10:44:24 +0100 Subject: [PATCH 006/110] Support ExtractorFactory in DefaultMediaSourceFactory. This allows to customize extractor flags more easily when setting up the player. In addition, we need to provide a way to pass in the ExtractorFactory through the constructor chain starting in SimpleExoPlayer so that removing the DefaultExtractorsFactory is possible for R8. PiperOrigin-RevId: 330472935 --- .../android/exoplayer2/SimpleExoPlayer.java | 36 ++++++++++++++--- .../exoplayer2/offline/DownloadHelper.java | 3 +- .../source/DefaultMediaSourceFactory.java | 40 ++++++++++++------- .../exoplayer2/source/MediaSourceFactory.java | 10 +++-- .../extractor/ExtractorsFactory.java | 6 +++ 5 files changed, 71 insertions(+), 24 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java b/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java index 787946d6a9..ba3a375a37 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java @@ -38,6 +38,8 @@ import com.google.android.exoplayer2.audio.AuxEffectInfo; import com.google.android.exoplayer2.decoder.DecoderCounters; import com.google.android.exoplayer2.device.DeviceInfo; import com.google.android.exoplayer2.device.DeviceListener; +import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory; +import com.google.android.exoplayer2.extractor.ExtractorsFactory; import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.metadata.MetadataOutput; import com.google.android.exoplayer2.source.DefaultMediaSourceFactory; @@ -52,6 +54,7 @@ import com.google.android.exoplayer2.trackselection.TrackSelectionArray; import com.google.android.exoplayer2.trackselection.TrackSelector; import com.google.android.exoplayer2.upstream.BandwidthMeter; import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter; +import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Clock; import com.google.android.exoplayer2.util.Log; @@ -115,9 +118,11 @@ public class SimpleExoPlayer extends BasePlayer /** * Creates a builder. * - *

Use {@link #Builder(Context, RenderersFactory)} instead, if you intend to provide a custom - * {@link RenderersFactory}. This is to ensure that ProGuard or R8 can remove ExoPlayer's {@link - * DefaultRenderersFactory} from the APK. + *

Use {@link #Builder(Context, RenderersFactory)} or {@link #Builder(Context, + * RenderersFactory, ExtractorsFactory)} instead, if you intend to provide a custom {@link + * RenderersFactory} or a custom {@link ExtractorsFactory}. This is to ensure that ProGuard or + * R8 can remove ExoPlayer's {@link DefaultRenderersFactory} and {@link + * DefaultExtractorsFactory} from the APK. * *

The builder uses the following default values: * @@ -146,7 +151,7 @@ public class SimpleExoPlayer extends BasePlayer * @param context A {@link Context}. */ public Builder(Context context) { - this(context, new DefaultRenderersFactory(context)); + this(context, new DefaultRenderersFactory(context), new DefaultExtractorsFactory()); } /** @@ -159,11 +164,30 @@ public class SimpleExoPlayer extends BasePlayer * player. */ public Builder(Context context, RenderersFactory renderersFactory) { + this(context, renderersFactory, new DefaultExtractorsFactory()); + } + + /** + * Creates a builder with a custom {@link RenderersFactory} and {@link ExtractorsFactory}. + * + *

See {@link #Builder(Context)} for a list of default values. + * + * @param context A {@link Context}. + * @param renderersFactory A factory for creating {@link Renderer Renderers} to be used by the + * player. + * @param extractorsFactory An {@link ExtractorsFactory} used to extract progressive media from + * its container. + */ + public Builder( + Context context, RenderersFactory renderersFactory, ExtractorsFactory extractorsFactory) { this( context, renderersFactory, new DefaultTrackSelector(context), - new DefaultMediaSourceFactory(context), + new DefaultMediaSourceFactory( + new DefaultDataSourceFactory( + context, Util.getUserAgent(context, ExoPlayerLibraryInfo.VERSION_SLASHY)), + extractorsFactory), new DefaultLoadControl(), DefaultBandwidthMeter.getSingletonInstance(context), new AnalyticsCollector(Clock.DEFAULT)); @@ -546,7 +570,7 @@ public class SimpleExoPlayer extends BasePlayer Clock clock, Looper applicationLooper) { this( - new Builder(context, renderersFactory) + new Builder(context, renderersFactory, new DefaultExtractorsFactory()) .setTrackSelector(trackSelector) .setMediaSourceFactory(mediaSourceFactory) .setLoadControl(loadControl) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadHelper.java b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadHelper.java index dd868f9822..ba8a799381 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadHelper.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/offline/DownloadHelper.java @@ -34,6 +34,7 @@ import com.google.android.exoplayer2.RenderersFactory; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.audio.AudioRendererEventListener; import com.google.android.exoplayer2.drm.DrmSessionManager; +import com.google.android.exoplayer2.extractor.ExtractorsFactory; import com.google.android.exoplayer2.source.DefaultMediaSourceFactory; import com.google.android.exoplayer2.source.MediaPeriod; import com.google.android.exoplayer2.source.MediaSource; @@ -889,7 +890,7 @@ public final class DownloadHelper { MediaItem mediaItem, DataSource.Factory dataSourceFactory, @Nullable DrmSessionManager drmSessionManager) { - return new DefaultMediaSourceFactory(dataSourceFactory) + return new DefaultMediaSourceFactory(dataSourceFactory, ExtractorsFactory.EMPTY) .setDrmSessionManager(drmSessionManager) .createMediaSource(mediaItem); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/DefaultMediaSourceFactory.java b/library/core/src/main/java/com/google/android/exoplayer2/source/DefaultMediaSourceFactory.java index 89b4ffb6a5..df1f3e2cf0 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/DefaultMediaSourceFactory.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/DefaultMediaSourceFactory.java @@ -23,13 +23,14 @@ import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlayerLibraryInfo; import com.google.android.exoplayer2.MediaItem; import com.google.android.exoplayer2.drm.DrmSessionManager; +import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory; +import com.google.android.exoplayer2.extractor.ExtractorsFactory; import com.google.android.exoplayer2.offline.StreamKey; import com.google.android.exoplayer2.source.ads.AdsLoader; import com.google.android.exoplayer2.source.ads.AdsLoader.AdViewProvider; import com.google.android.exoplayer2.source.ads.AdsMediaSource; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory; -import com.google.android.exoplayer2.upstream.DefaultLoadErrorHandlingPolicy; import com.google.android.exoplayer2.upstream.HttpDataSource; import com.google.android.exoplayer2.upstream.LoadErrorHandlingPolicy; import com.google.android.exoplayer2.util.Assertions; @@ -64,9 +65,10 @@ import java.util.List; *

  • {@link ProgressiveMediaSource.Factory} serves as a fallback if the item's {@link * MediaItem.PlaybackProperties#uri uri} doesn't match one of the above. It tries to infer the * required extractor by using the {@link - * com.google.android.exoplayer2.extractor.DefaultExtractorsFactory}. An {@link - * UnrecognizedInputFormatException} is thrown if none of the available extractors can read - * the stream. + * com.google.android.exoplayer2.extractor.DefaultExtractorsFactory} or the {@link + * ExtractorsFactory} provided in {@link #DefaultMediaSourceFactory(DataSource.Factory, + * ExtractorsFactory)}. An {@link UnrecognizedInputFormatException} is thrown if none of the + * available extractors can read the stream. * * *

    Ad support for media items with ad tag URIs

    @@ -105,6 +107,7 @@ public final class DefaultMediaSourceFactory implements MediaSourceFactory { @Nullable private AdViewProvider adViewProvider; @Nullable private DrmSessionManager drmSessionManager; @Nullable private List streamKeys; + @Nullable private LoadErrorHandlingPolicy loadErrorHandlingPolicy; /** * Creates a new instance. @@ -124,9 +127,22 @@ public final class DefaultMediaSourceFactory implements MediaSourceFactory { * for requesting media data. */ public DefaultMediaSourceFactory(DataSource.Factory dataSourceFactory) { + this(dataSourceFactory, new DefaultExtractorsFactory()); + } + + /** + * Creates a new instance. + * + * @param dataSourceFactory A {@link DataSource.Factory} to create {@link DataSource} instances + * for requesting media data. + * @param extractorsFactory An {@link ExtractorsFactory} used to extract progressive media from + * its container. + */ + public DefaultMediaSourceFactory( + DataSource.Factory dataSourceFactory, ExtractorsFactory extractorsFactory) { this.dataSourceFactory = dataSourceFactory; mediaSourceDrmHelper = new MediaSourceDrmHelper(); - mediaSourceFactories = loadDelegates(dataSourceFactory); + mediaSourceFactories = loadDelegates(dataSourceFactory, extractorsFactory); supportedTypes = new int[mediaSourceFactories.size()]; for (int i = 0; i < mediaSourceFactories.size(); i++) { supportedTypes[i] = mediaSourceFactories.keyAt(i); @@ -180,13 +196,7 @@ public final class DefaultMediaSourceFactory implements MediaSourceFactory { @Override public DefaultMediaSourceFactory setLoadErrorHandlingPolicy( @Nullable LoadErrorHandlingPolicy loadErrorHandlingPolicy) { - LoadErrorHandlingPolicy newLoadErrorHandlingPolicy = - loadErrorHandlingPolicy != null - ? loadErrorHandlingPolicy - : new DefaultLoadErrorHandlingPolicy(); - for (int i = 0; i < mediaSourceFactories.size(); i++) { - mediaSourceFactories.valueAt(i).setLoadErrorHandlingPolicy(newLoadErrorHandlingPolicy); - } + this.loadErrorHandlingPolicy = loadErrorHandlingPolicy; return this; } @@ -224,6 +234,7 @@ public final class DefaultMediaSourceFactory implements MediaSourceFactory { !mediaItem.playbackProperties.streamKeys.isEmpty() ? mediaItem.playbackProperties.streamKeys : streamKeys); + mediaSourceFactory.setLoadErrorHandlingPolicy(loadErrorHandlingPolicy); MediaSource mediaSource = mediaSourceFactory.createMediaSource(mediaItem); @@ -285,7 +296,7 @@ public final class DefaultMediaSourceFactory implements MediaSourceFactory { } private static SparseArray loadDelegates( - DataSource.Factory dataSourceFactory) { + DataSource.Factory dataSourceFactory, ExtractorsFactory extractorsFactory) { SparseArray factories = new SparseArray<>(); // LINT.IfChange try { @@ -320,7 +331,8 @@ public final class DefaultMediaSourceFactory implements MediaSourceFactory { // Expected if the app was built without the hls module. } // LINT.ThenChange(../../../../../../../../proguard-rules.txt) - factories.put(C.TYPE_OTHER, new ProgressiveMediaSource.Factory(dataSourceFactory)); + factories.put( + C.TYPE_OTHER, new ProgressiveMediaSource.Factory(dataSourceFactory, extractorsFactory)); return factories; } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSourceFactory.java b/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSourceFactory.java index 4175121d38..204220e334 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSourceFactory.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSourceFactory.java @@ -24,6 +24,7 @@ import com.google.android.exoplayer2.drm.DrmSessionManager; import com.google.android.exoplayer2.drm.HttpMediaDrmCallback; import com.google.android.exoplayer2.offline.StreamKey; import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory; +import com.google.android.exoplayer2.upstream.DefaultLoadErrorHandlingPolicy; import com.google.android.exoplayer2.upstream.HttpDataSource; import com.google.android.exoplayer2.upstream.LoadErrorHandlingPolicy; import java.util.List; @@ -59,7 +60,8 @@ public interface MediaSourceFactory { * Sets the {@link DrmSessionManager} to use for all media items regardless of their {@link * MediaItem.DrmConfiguration}. * - * @param drmSessionManager The {@link DrmSessionManager}. + * @param drmSessionManager The {@link DrmSessionManager}, or {@code null} to use the {@link + * DefaultDrmSessionManager}. * @return This factory, for convenience. */ MediaSourceFactory setDrmSessionManager(@Nullable DrmSessionManager drmSessionManager); @@ -85,7 +87,8 @@ public interface MediaSourceFactory { * #setDrmHttpDataSourceFactory(HttpDataSource.Factory)} or a {@link DrmSessionManager} has been * set by {@link #setDrmSessionManager(DrmSessionManager)}, this user agent is ignored. * - * @param userAgent The user agent to be used for DRM requests. + * @param userAgent The user agent to be used for DRM requests, or {@code null} to use the + * default. * @return This factory, for convenience. */ MediaSourceFactory setDrmUserAgent(@Nullable String userAgent); @@ -93,7 +96,8 @@ public interface MediaSourceFactory { /** * Sets an optional {@link LoadErrorHandlingPolicy}. * - * @param loadErrorHandlingPolicy A {@link LoadErrorHandlingPolicy}. + * @param loadErrorHandlingPolicy A {@link LoadErrorHandlingPolicy}, or {@code null} to use the + * {@link DefaultLoadErrorHandlingPolicy}. * @return This factory, for convenience. */ MediaSourceFactory setLoadErrorHandlingPolicy( diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ExtractorsFactory.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ExtractorsFactory.java index d077b1b11e..97ae74b9d2 100644 --- a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ExtractorsFactory.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ExtractorsFactory.java @@ -22,6 +22,12 @@ import java.util.Map; /** Factory for arrays of {@link Extractor} instances. */ public interface ExtractorsFactory { + /** + * Extractor factory that returns an empty list of extractors. Can be used whenever {@link + * Extractor Extractors} are not required. + */ + ExtractorsFactory EMPTY = () -> new Extractor[] {}; + /** Returns an array of new {@link Extractor} instances. */ Extractor[] createExtractors(); From 49699d398d5231b190d4d2a8f00488de822b5521 Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 8 Sep 2020 22:57:12 +0100 Subject: [PATCH 007/110] Make User-Agent optional PiperOrigin-RevId: 330593247 --- .../exoplayer2/gldemo/MainActivity.java | 7 +-- .../android/exoplayer2/demo/DemoUtil.java | 15 +---- .../exoplayer2/demo/PlayerActivity.java | 1 - .../exoplayer2/surfacedemo/MainActivity.java | 7 +-- .../ext/cronet/CronetDataSource.java | 12 ++++ .../ext/cronet/CronetDataSourceFactory.java | 60 +++++++++++++++---- .../ext/flac/FlacExtractorSeekTest.java | 3 +- .../exoplayer2/ext/flac/FlacPlaybackTest.java | 3 +- .../exoplayer2/ext/ima/ImaPlaybackTest.java | 6 +- .../exoplayer2/ext/media2/PlayerTestRule.java | 4 +- .../ext/okhttp/OkHttpDataSource.java | 18 ++++++ .../ext/okhttp/OkHttpDataSourceFactory.java | 20 +++++++ .../exoplayer2/ext/opus/OpusPlaybackTest.java | 3 +- .../exoplayer2/ext/vp9/VpxPlaybackTest.java | 3 +- .../exoplayer2/ExoPlayerLibraryInfo.java | 5 ++ .../source/DefaultMediaSourceFactory.java | 5 +- .../source/MediaSourceDrmHelper.java | 10 +--- .../upstream/DefaultDataSource.java | 15 +++++ .../upstream/DefaultDataSourceFactory.java | 7 +++ .../upstream/DefaultHttpDataSource.java | 25 +++++++- .../DefaultHttpDataSourceFactory.java | 25 +++++--- .../android/exoplayer2/ExoPlayerTest.java | 10 +--- .../extractor/amr/AmrExtractorSeekTest.java | 2 +- .../extractor/flac/FlacExtractorSeekTest.java | 3 +- .../mp3/ConstantBitrateSeekerTest.java | 2 +- .../extractor/mp3/IndexSeekerTest.java | 2 +- .../extractor/ts/AdtsExtractorSeekTest.java | 2 +- .../extractor/ts/PsExtractorSeekTest.java | 2 +- .../extractor/ts/TsExtractorSeekTest.java | 2 +- .../playbacktests/gts/DashTestRunner.java | 7 +-- .../exoplayer2/testutil/ExoHostedTest.java | 10 +--- 31 files changed, 196 insertions(+), 100 deletions(-) diff --git a/demos/gl/src/main/java/com/google/android/exoplayer2/gldemo/MainActivity.java b/demos/gl/src/main/java/com/google/android/exoplayer2/gldemo/MainActivity.java index 6944eb662d..dc0a8b990a 100644 --- a/demos/gl/src/main/java/com/google/android/exoplayer2/gldemo/MainActivity.java +++ b/demos/gl/src/main/java/com/google/android/exoplayer2/gldemo/MainActivity.java @@ -139,13 +139,12 @@ public final class MainActivity extends Activity { ACTION_VIEW.equals(action) ? Assertions.checkNotNull(intent.getData()) : Uri.parse(DEFAULT_MEDIA_URI); - String userAgent = Util.getUserAgent(this, getString(R.string.application_name)); DrmSessionManager drmSessionManager; if (Util.SDK_INT >= 18 && intent.hasExtra(DRM_SCHEME_EXTRA)) { String drmScheme = Assertions.checkNotNull(intent.getStringExtra(DRM_SCHEME_EXTRA)); String drmLicenseUrl = Assertions.checkNotNull(intent.getStringExtra(DRM_LICENSE_URL_EXTRA)); UUID drmSchemeUuid = Assertions.checkNotNull(Util.getDrmUuid(drmScheme)); - HttpDataSource.Factory licenseDataSourceFactory = new DefaultHttpDataSourceFactory(userAgent); + HttpDataSource.Factory licenseDataSourceFactory = new DefaultHttpDataSourceFactory(); HttpMediaDrmCallback drmCallback = new HttpMediaDrmCallback(drmLicenseUrl, licenseDataSourceFactory); drmSessionManager = @@ -156,9 +155,7 @@ public final class MainActivity extends Activity { drmSessionManager = DrmSessionManager.getDummyDrmSessionManager(); } - DataSource.Factory dataSourceFactory = - new DefaultDataSourceFactory( - this, Util.getUserAgent(this, getString(R.string.application_name))); + DataSource.Factory dataSourceFactory = new DefaultDataSourceFactory(this); MediaSource mediaSource; @C.ContentType int type = Util.inferContentType(uri, intent.getStringExtra(EXTENSION_EXTRA)); if (type == C.TYPE_DASH) { diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/DemoUtil.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/DemoUtil.java index c3621879c5..2d15dfcbb4 100644 --- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/DemoUtil.java +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/DemoUtil.java @@ -34,7 +34,6 @@ import com.google.android.exoplayer2.upstream.cache.CacheDataSource; import com.google.android.exoplayer2.upstream.cache.NoOpCacheEvictor; import com.google.android.exoplayer2.upstream.cache.SimpleCache; import com.google.android.exoplayer2.util.Log; -import com.google.android.exoplayer2.util.Util; import java.io.File; import java.io.IOException; import java.util.concurrent.Executors; @@ -50,7 +49,6 @@ public final class DemoUtil { private static final String DOWNLOAD_TRACKER_ACTION_FILE = "tracked_actions"; private static final String DOWNLOAD_CONTENT_DIRECTORY = "downloads"; - private static @MonotonicNonNull String userAgent; private static DataSource.@MonotonicNonNull Factory dataSourceFactory; private static HttpDataSource.@MonotonicNonNull Factory httpDataSourceFactory; private static @MonotonicNonNull DatabaseProvider databaseProvider; @@ -78,23 +76,12 @@ public final class DemoUtil { .setExtensionRendererMode(extensionRendererMode); } - public static synchronized String getUserAgent(Context context) { - if (userAgent == null) { - userAgent = Util.getUserAgent(context, "ExoPlayerDemo"); - } - return userAgent; - } - public static synchronized HttpDataSource.Factory getHttpDataSourceFactory(Context context) { if (httpDataSourceFactory == null) { context = context.getApplicationContext(); CronetEngineWrapper cronetEngineWrapper = new CronetEngineWrapper(context); httpDataSourceFactory = - new CronetDataSourceFactory( - cronetEngineWrapper, - Executors.newSingleThreadExecutor(), - /* transferListener= */ null, - getUserAgent(context)); + new CronetDataSourceFactory(cronetEngineWrapper, Executors.newSingleThreadExecutor()); } return httpDataSourceFactory; } diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java index c9af9f77bb..49fe440101 100644 --- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java @@ -288,7 +288,6 @@ public class PlayerActivity extends AppCompatActivity DemoUtil.buildRenderersFactory(/* context= */ this, preferExtensionDecoders); MediaSourceFactory mediaSourceFactory = new DefaultMediaSourceFactory(dataSourceFactory) - .setDrmUserAgent(DemoUtil.getUserAgent(this)) .setAdsLoaderProvider(this::getAdsLoader) .setAdViewProvider(playerView); diff --git a/demos/surface/src/main/java/com/google/android/exoplayer2/surfacedemo/MainActivity.java b/demos/surface/src/main/java/com/google/android/exoplayer2/surfacedemo/MainActivity.java index 1cd5c128c1..eb669ecf94 100644 --- a/demos/surface/src/main/java/com/google/android/exoplayer2/surfacedemo/MainActivity.java +++ b/demos/surface/src/main/java/com/google/android/exoplayer2/surfacedemo/MainActivity.java @@ -184,13 +184,12 @@ public final class MainActivity extends Activity { ACTION_VIEW.equals(action) ? Assertions.checkNotNull(intent.getData()) : Uri.parse(DEFAULT_MEDIA_URI); - String userAgent = Util.getUserAgent(this, getString(R.string.application_name)); DrmSessionManager drmSessionManager; if (intent.hasExtra(DRM_SCHEME_EXTRA)) { String drmScheme = Assertions.checkNotNull(intent.getStringExtra(DRM_SCHEME_EXTRA)); String drmLicenseUrl = Assertions.checkNotNull(intent.getStringExtra(DRM_LICENSE_URL_EXTRA)); UUID drmSchemeUuid = Assertions.checkNotNull(Util.getDrmUuid(drmScheme)); - HttpDataSource.Factory licenseDataSourceFactory = new DefaultHttpDataSourceFactory(userAgent); + HttpDataSource.Factory licenseDataSourceFactory = new DefaultHttpDataSourceFactory(); HttpMediaDrmCallback drmCallback = new HttpMediaDrmCallback(drmLicenseUrl, licenseDataSourceFactory); drmSessionManager = @@ -201,9 +200,7 @@ public final class MainActivity extends Activity { drmSessionManager = DrmSessionManager.getDummyDrmSessionManager(); } - DataSource.Factory dataSourceFactory = - new DefaultDataSourceFactory( - this, Util.getUserAgent(this, getString(R.string.application_name))); + DataSource.Factory dataSourceFactory = new DefaultDataSourceFactory(this); MediaSource mediaSource; @C.ContentType int type = Util.inferContentType(uri, intent.getStringExtra(EXTENSION_EXTRA)); if (type == C.TYPE_DASH) { diff --git a/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetDataSource.java b/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetDataSource.java index 1173b16a21..26a60d3332 100644 --- a/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetDataSource.java +++ b/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetDataSource.java @@ -150,6 +150,8 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource { private volatile long currentConnectTimeoutMs; /** + * Creates an instance. + * * @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 @@ -168,6 +170,8 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource { } /** + * Creates an instance. + * * @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 @@ -199,6 +203,8 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource { } /** + * Creates an instance. + * * @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 @@ -233,6 +239,8 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource { } /** + * Creates an instance. + * * @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 @@ -262,6 +270,8 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource { } /** + * Creates an instance. + * * @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 @@ -301,6 +311,8 @@ public class CronetDataSource extends BaseDataSource implements HttpDataSource { } /** + * Creates an instance. + * * @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 diff --git a/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetDataSourceFactory.java b/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetDataSourceFactory.java index 4086011b4f..4590936ea5 100644 --- a/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetDataSourceFactory.java +++ b/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetDataSourceFactory.java @@ -15,6 +15,8 @@ */ package com.google.android.exoplayer2.ext.cronet; +import static com.google.android.exoplayer2.ExoPlayerLibraryInfo.DEFAULT_USER_AGENT; + import androidx.annotation.Nullable; import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory; import com.google.android.exoplayer2.upstream.HttpDataSource; @@ -50,7 +52,7 @@ public final class CronetDataSourceFactory extends BaseFactory { private final HttpDataSource.Factory fallbackFactory; /** - * Constructs a CronetDataSourceFactory. + * Creates an instance. * *

    If the {@link CronetEngineWrapper} fails to provide a {@link CronetEngine}, the provided * fallback {@link HttpDataSource.Factory} will be used instead. @@ -79,7 +81,24 @@ public final class CronetDataSourceFactory extends BaseFactory { } /** - * Constructs a CronetDataSourceFactory. + * Creates an instance. + * + *

    If the {@link CronetEngineWrapper} fails to provide a {@link CronetEngine}, a {@link + * DefaultHttpDataSourceFactory} will be used instead. + * + *

    Sets {@link CronetDataSource#DEFAULT_CONNECT_TIMEOUT_MILLIS} as the connection timeout, + * {@link CronetDataSource#DEFAULT_READ_TIMEOUT_MILLIS} as the read timeout and disables + * cross-protocol redirects. + * + * @param cronetEngineWrapper A {@link CronetEngineWrapper}. + * @param executor The {@link java.util.concurrent.Executor} that will perform the requests. + */ + public CronetDataSourceFactory(CronetEngineWrapper cronetEngineWrapper, Executor executor) { + this(cronetEngineWrapper, executor, DEFAULT_USER_AGENT); + } + + /** + * Creates an instance. * *

    If the {@link CronetEngineWrapper} fails to provide a {@link CronetEngine}, a {@link * DefaultHttpDataSourceFactory} will be used instead. @@ -93,9 +112,7 @@ public final class CronetDataSourceFactory extends BaseFactory { * @param userAgent A user agent used to create a fallback HttpDataSource if needed. */ public CronetDataSourceFactory( - CronetEngineWrapper cronetEngineWrapper, - Executor executor, - String userAgent) { + CronetEngineWrapper cronetEngineWrapper, Executor executor, String userAgent) { this( cronetEngineWrapper, executor, @@ -112,7 +129,7 @@ public final class CronetDataSourceFactory extends BaseFactory { } /** - * Constructs a CronetDataSourceFactory. + * Creates an instance. * *

    If the {@link CronetEngineWrapper} fails to provide a {@link CronetEngine}, a {@link * DefaultHttpDataSourceFactory} will be used instead. @@ -147,7 +164,7 @@ public final class CronetDataSourceFactory extends BaseFactory { } /** - * Constructs a CronetDataSourceFactory. + * Creates an instance. * *

    If the {@link CronetEngineWrapper} fails to provide a {@link CronetEngine}, the provided * fallback {@link HttpDataSource.Factory} will be used instead. @@ -178,7 +195,7 @@ public final class CronetDataSourceFactory extends BaseFactory { } /** - * Constructs a CronetDataSourceFactory. + * Creates an instance. * *

    If the {@link CronetEngineWrapper} fails to provide a {@link CronetEngine}, the provided * fallback {@link HttpDataSource.Factory} will be used instead. @@ -209,7 +226,28 @@ public final class CronetDataSourceFactory extends BaseFactory { } /** - * Constructs a CronetDataSourceFactory. + * Creates an instance. + * + *

    If the {@link CronetEngineWrapper} fails to provide a {@link CronetEngine}, a {@link + * DefaultHttpDataSourceFactory} will be used instead. + * + *

    Sets {@link CronetDataSource#DEFAULT_CONNECT_TIMEOUT_MILLIS} as the connection timeout, + * {@link CronetDataSource#DEFAULT_READ_TIMEOUT_MILLIS} as the read timeout and disables + * cross-protocol redirects. + * + * @param cronetEngineWrapper A {@link CronetEngineWrapper}. + * @param executor The {@link java.util.concurrent.Executor} that will perform the requests. + * @param transferListener An optional listener. + */ + public CronetDataSourceFactory( + CronetEngineWrapper cronetEngineWrapper, + Executor executor, + @Nullable TransferListener transferListener) { + this(cronetEngineWrapper, executor, transferListener, DEFAULT_USER_AGENT); + } + + /** + * Creates an instance. * *

    If the {@link CronetEngineWrapper} fails to provide a {@link CronetEngine}, a {@link * DefaultHttpDataSourceFactory} will be used instead. @@ -244,7 +282,7 @@ public final class CronetDataSourceFactory extends BaseFactory { } /** - * Constructs a CronetDataSourceFactory. + * Creates an instance. * *

    If the {@link CronetEngineWrapper} fails to provide a {@link CronetEngine}, a {@link * DefaultHttpDataSourceFactory} will be used instead. @@ -277,7 +315,7 @@ public final class CronetDataSourceFactory extends BaseFactory { } /** - * Constructs a CronetDataSourceFactory. + * Creates an instance. * *

    If the {@link CronetEngineWrapper} fails to provide a {@link CronetEngine}, the provided * fallback {@link HttpDataSource.Factory} will be used instead. diff --git a/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacExtractorSeekTest.java b/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacExtractorSeekTest.java index ba9e69410d..e6e66fbe29 100644 --- a/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacExtractorSeekTest.java +++ b/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacExtractorSeekTest.java @@ -46,8 +46,7 @@ public final class FlacExtractorSeekTest { private FlacExtractor extractor = new FlacExtractor(); private FakeExtractorOutput extractorOutput = new FakeExtractorOutput(); private DefaultDataSource dataSource = - new DefaultDataSourceFactory(ApplicationProvider.getApplicationContext(), "UserAgent") - .createDataSource(); + new DefaultDataSourceFactory(ApplicationProvider.getApplicationContext()).createDataSource(); @Test public void flacExtractorReads_seekTable_returnSeekableSeekMap() throws IOException { diff --git a/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacPlaybackTest.java b/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacPlaybackTest.java index 90ec83630b..bbcc26fb64 100644 --- a/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacPlaybackTest.java +++ b/extensions/flac/src/androidTest/java/com/google/android/exoplayer2/ext/flac/FlacPlaybackTest.java @@ -111,8 +111,7 @@ public class FlacPlaybackTest { player.addListener(this); MediaSource mediaSource = new ProgressiveMediaSource.Factory( - new DefaultDataSourceFactory(context, "ExoPlayerExtFlacTest"), - MatroskaExtractor.FACTORY) + new DefaultDataSourceFactory(context), MatroskaExtractor.FACTORY) .createMediaSource(MediaItem.fromUri(uri)); player.setMediaSource(mediaSource); player.prepare(); diff --git a/extensions/ima/src/androidTest/java/com/google/android/exoplayer2/ext/ima/ImaPlaybackTest.java b/extensions/ima/src/androidTest/java/com/google/android/exoplayer2/ext/ima/ImaPlaybackTest.java index cd58e1f58b..88bc4e14c5 100644 --- a/extensions/ima/src/androidTest/java/com/google/android/exoplayer2/ext/ima/ImaPlaybackTest.java +++ b/extensions/ima/src/androidTest/java/com/google/android/exoplayer2/ext/ima/ImaPlaybackTest.java @@ -49,7 +49,6 @@ import com.google.android.exoplayer2.trackselection.MappingTrackSelector; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory; import com.google.android.exoplayer2.util.Assertions; -import com.google.android.exoplayer2.util.Util; import com.google.common.collect.ImmutableList; import java.util.ArrayList; import java.util.Arrays; @@ -235,13 +234,10 @@ public final class ImaPlaybackTest { @Override protected MediaSource buildSource( HostActivity host, - String userAgent, DrmSessionManager drmSessionManager, FrameLayout overlayFrameLayout) { Context context = host.getApplicationContext(); - DataSource.Factory dataSourceFactory = - new DefaultDataSourceFactory( - context, Util.getUserAgent(context, ImaPlaybackTest.class.getSimpleName())); + DataSource.Factory dataSourceFactory = new DefaultDataSourceFactory(context); MediaSource contentMediaSource = new DefaultMediaSourceFactory(context).createMediaSource(MediaItem.fromUri(contentUri)); return new AdsMediaSource( diff --git a/extensions/media2/src/androidTest/java/com/google/android/exoplayer2/ext/media2/PlayerTestRule.java b/extensions/media2/src/androidTest/java/com/google/android/exoplayer2/ext/media2/PlayerTestRule.java index f5518e0c7c..df6963c2fc 100644 --- a/extensions/media2/src/androidTest/java/com/google/android/exoplayer2/ext/media2/PlayerTestRule.java +++ b/extensions/media2/src/androidTest/java/com/google/android/exoplayer2/ext/media2/PlayerTestRule.java @@ -27,7 +27,6 @@ import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DataSpec; import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory; import com.google.android.exoplayer2.upstream.TransferListener; -import com.google.android.exoplayer2.util.Util; import java.io.IOException; import java.util.List; import java.util.Map; @@ -128,8 +127,7 @@ import org.junit.rules.ExternalResource; private final DefaultDataSourceFactory defaultDataSourceFactory; public InstrumentingDataSourceFactory(Context context) { - defaultDataSourceFactory = - new DefaultDataSourceFactory(context, Util.getUserAgent(context, "media2-test")); + defaultDataSourceFactory = new DefaultDataSourceFactory(context); } @Override diff --git a/extensions/okhttp/src/main/java/com/google/android/exoplayer2/ext/okhttp/OkHttpDataSource.java b/extensions/okhttp/src/main/java/com/google/android/exoplayer2/ext/okhttp/OkHttpDataSource.java index 2f43ec4cf8..57fee20d04 100644 --- a/extensions/okhttp/src/main/java/com/google/android/exoplayer2/ext/okhttp/OkHttpDataSource.java +++ b/extensions/okhttp/src/main/java/com/google/android/exoplayer2/ext/okhttp/OkHttpDataSource.java @@ -81,6 +81,18 @@ public class OkHttpDataSource extends BaseDataSource implements HttpDataSource { private long bytesRead; /** + * Creates an instance. + * + * @param callFactory A {@link Call.Factory} (typically an {@link okhttp3.OkHttpClient}) for use + * by the source. + */ + public OkHttpDataSource(Call.Factory callFactory) { + this(callFactory, ExoPlayerLibraryInfo.DEFAULT_USER_AGENT); + } + + /** + * Creates an instance. + * * @param callFactory A {@link Call.Factory} (typically an {@link okhttp3.OkHttpClient}) for use * by the source. * @param userAgent An optional User-Agent string. @@ -90,6 +102,8 @@ public class OkHttpDataSource extends BaseDataSource implements HttpDataSource { } /** + * Creates an instance. + * * @param callFactory A {@link Call.Factory} (typically an {@link okhttp3.OkHttpClient}) for use * by the source. * @param userAgent An optional User-Agent string. @@ -111,6 +125,8 @@ public class OkHttpDataSource extends BaseDataSource implements HttpDataSource { } /** + * Creates an instance. + * * @param callFactory A {@link Call.Factory} (typically an {@link okhttp3.OkHttpClient}) for use * by the source. * @param userAgent An optional User-Agent string. @@ -135,6 +151,8 @@ public class OkHttpDataSource extends BaseDataSource implements HttpDataSource { } /** + * Creates an instance. + * * @param callFactory A {@link Call.Factory} (typically an {@link okhttp3.OkHttpClient}) for use * by the source. * @param userAgent An optional User-Agent string. diff --git a/extensions/okhttp/src/main/java/com/google/android/exoplayer2/ext/okhttp/OkHttpDataSourceFactory.java b/extensions/okhttp/src/main/java/com/google/android/exoplayer2/ext/okhttp/OkHttpDataSourceFactory.java index f3d74f9233..728428c811 100644 --- a/extensions/okhttp/src/main/java/com/google/android/exoplayer2/ext/okhttp/OkHttpDataSourceFactory.java +++ b/extensions/okhttp/src/main/java/com/google/android/exoplayer2/ext/okhttp/OkHttpDataSourceFactory.java @@ -15,6 +15,8 @@ */ package com.google.android.exoplayer2.ext.okhttp; +import static com.google.android.exoplayer2.ExoPlayerLibraryInfo.DEFAULT_USER_AGENT; + import androidx.annotation.Nullable; import com.google.android.exoplayer2.upstream.HttpDataSource; import com.google.android.exoplayer2.upstream.HttpDataSource.BaseFactory; @@ -34,6 +36,18 @@ public final class OkHttpDataSourceFactory extends BaseFactory { @Nullable private final CacheControl cacheControl; /** + * Creates an instance. + * + * @param callFactory A {@link Call.Factory} (typically an {@link okhttp3.OkHttpClient}) for use + * by the sources created by the factory. + */ + public OkHttpDataSourceFactory(Call.Factory callFactory) { + this(callFactory, DEFAULT_USER_AGENT, /* listener= */ null, /* cacheControl= */ null); + } + + /** + * Creates an instance. + * * @param callFactory A {@link Call.Factory} (typically an {@link okhttp3.OkHttpClient}) for use * by the sources created by the factory. * @param userAgent An optional User-Agent string. @@ -43,6 +57,8 @@ public final class OkHttpDataSourceFactory extends BaseFactory { } /** + * Creates an instance. + * * @param callFactory A {@link Call.Factory} (typically an {@link okhttp3.OkHttpClient}) for use * by the sources created by the factory. * @param userAgent An optional User-Agent string. @@ -54,6 +70,8 @@ public final class OkHttpDataSourceFactory extends BaseFactory { } /** + * Creates an instance. + * * @param callFactory A {@link Call.Factory} (typically an {@link okhttp3.OkHttpClient}) for use * by the sources created by the factory. * @param userAgent An optional User-Agent string. @@ -65,6 +83,8 @@ public final class OkHttpDataSourceFactory extends BaseFactory { } /** + * Creates an instance. + * * @param callFactory A {@link Call.Factory} (typically an {@link okhttp3.OkHttpClient}) for use * by the sources created by the factory. * @param userAgent An optional User-Agent string. diff --git a/extensions/opus/src/androidTest/java/com/google/android/exoplayer2/ext/opus/OpusPlaybackTest.java b/extensions/opus/src/androidTest/java/com/google/android/exoplayer2/ext/opus/OpusPlaybackTest.java index 2d0b632c2e..c964b0cc1c 100644 --- a/extensions/opus/src/androidTest/java/com/google/android/exoplayer2/ext/opus/OpusPlaybackTest.java +++ b/extensions/opus/src/androidTest/java/com/google/android/exoplayer2/ext/opus/OpusPlaybackTest.java @@ -92,8 +92,7 @@ public class OpusPlaybackTest { player.addListener(this); MediaSource mediaSource = new ProgressiveMediaSource.Factory( - new DefaultDataSourceFactory(context, "ExoPlayerExtOpusTest"), - MatroskaExtractor.FACTORY) + new DefaultDataSourceFactory(context), MatroskaExtractor.FACTORY) .createMediaSource(MediaItem.fromUri(uri)); player.setMediaSource(mediaSource); player.prepare(); diff --git a/extensions/vp9/src/androidTest/java/com/google/android/exoplayer2/ext/vp9/VpxPlaybackTest.java b/extensions/vp9/src/androidTest/java/com/google/android/exoplayer2/ext/vp9/VpxPlaybackTest.java index aec52755ea..823ce02cfe 100644 --- a/extensions/vp9/src/androidTest/java/com/google/android/exoplayer2/ext/vp9/VpxPlaybackTest.java +++ b/extensions/vp9/src/androidTest/java/com/google/android/exoplayer2/ext/vp9/VpxPlaybackTest.java @@ -121,8 +121,7 @@ public class VpxPlaybackTest { player.addListener(this); MediaSource mediaSource = new ProgressiveMediaSource.Factory( - new DefaultDataSourceFactory(context, "ExoPlayerExtVp9Test"), - MatroskaExtractor.FACTORY) + new DefaultDataSourceFactory(context), MatroskaExtractor.FACTORY) .createMediaSource(MediaItem.fromUri(uri)); player .createMessage(videoRenderer) diff --git a/library/common/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java b/library/common/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java index 4ee35cbdc3..15c4bf1c1d 100644 --- a/library/common/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java +++ b/library/common/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2; +import android.os.Build; import java.util.HashSet; /** @@ -45,6 +46,10 @@ public final class ExoPlayerLibraryInfo { // Intentionally hardcoded. Do not derive from other constants (e.g. VERSION) or vice versa. public static final int VERSION_INT = 2012000; + /** The default user agent for requests made by the library. */ + public static final String DEFAULT_USER_AGENT = + VERSION_SLASHY + " (Linux;Android " + Build.VERSION.RELEASE + ") " + VERSION_SLASHY; + /** * Whether the library was compiled with {@link com.google.android.exoplayer2.util.Assertions} * checks enabled. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/DefaultMediaSourceFactory.java b/library/core/src/main/java/com/google/android/exoplayer2/source/DefaultMediaSourceFactory.java index df1f3e2cf0..f4fc7e4afe 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/DefaultMediaSourceFactory.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/DefaultMediaSourceFactory.java @@ -20,7 +20,6 @@ import android.net.Uri; import android.util.SparseArray; import androidx.annotation.Nullable; import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.ExoPlayerLibraryInfo; import com.google.android.exoplayer2.MediaItem; import com.google.android.exoplayer2.drm.DrmSessionManager; import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory; @@ -115,9 +114,7 @@ public final class DefaultMediaSourceFactory implements MediaSourceFactory { * @param context Any context. */ public DefaultMediaSourceFactory(Context context) { - this( - new DefaultDataSourceFactory( - context, Util.getUserAgent(context, ExoPlayerLibraryInfo.VERSION_SLASHY))); + this(new DefaultDataSourceFactory(context)); } /** diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSourceDrmHelper.java b/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSourceDrmHelper.java index 29325d789e..7859254401 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSourceDrmHelper.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/MediaSourceDrmHelper.java @@ -15,12 +15,11 @@ */ package com.google.android.exoplayer2.source; +import static com.google.android.exoplayer2.ExoPlayerLibraryInfo.DEFAULT_USER_AGENT; import static com.google.android.exoplayer2.drm.DefaultDrmSessionManager.MODE_PLAYBACK; import static com.google.android.exoplayer2.util.Util.castNonNull; -import android.os.Build; import androidx.annotation.Nullable; -import com.google.android.exoplayer2.ExoPlayerLibraryInfo; import com.google.android.exoplayer2.MediaItem; import com.google.android.exoplayer2.drm.DefaultDrmSessionManager; import com.google.android.exoplayer2.drm.DrmSessionManager; @@ -36,13 +35,6 @@ import java.util.Map; /** A helper to create a {@link DrmSessionManager} from a {@link MediaItem}. */ public final class MediaSourceDrmHelper { - private static final String DEFAULT_USER_AGENT = - ExoPlayerLibraryInfo.VERSION_SLASHY - + " (Linux;Android " - + Build.VERSION.RELEASE - + ") " - + ExoPlayerLibraryInfo.VERSION_SLASHY; - @Nullable private HttpDataSource.Factory drmHttpDataSourceFactory; @Nullable private String userAgent; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultDataSource.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultDataSource.java index afef3e6761..7efa89eaa0 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultDataSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultDataSource.java @@ -18,6 +18,7 @@ package com.google.android.exoplayer2.upstream; import android.content.Context; import android.net.Uri; import androidx.annotation.Nullable; +import com.google.android.exoplayer2.ExoPlayerLibraryInfo; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Log; import com.google.android.exoplayer2.util.Util; @@ -74,6 +75,20 @@ public final class DefaultDataSource implements DataSource { @Nullable private DataSource dataSource; + /** + * Constructs a new instance, optionally configured to follow cross-protocol redirects. + * + * @param context A context. + */ + public DefaultDataSource(Context context, boolean allowCrossProtocolRedirects) { + this( + context, + ExoPlayerLibraryInfo.DEFAULT_USER_AGENT, + DefaultHttpDataSource.DEFAULT_CONNECT_TIMEOUT_MILLIS, + DefaultHttpDataSource.DEFAULT_READ_TIMEOUT_MILLIS, + allowCrossProtocolRedirects); + } + /** * Constructs a new instance, optionally configured to follow cross-protocol redirects. * diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultDataSourceFactory.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultDataSourceFactory.java index 6b1131a3bd..3b7cfa9d49 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultDataSourceFactory.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultDataSourceFactory.java @@ -15,6 +15,8 @@ */ package com.google.android.exoplayer2.upstream; +import static com.google.android.exoplayer2.ExoPlayerLibraryInfo.DEFAULT_USER_AGENT; + import android.content.Context; import androidx.annotation.Nullable; import com.google.android.exoplayer2.upstream.DataSource.Factory; @@ -29,6 +31,11 @@ public final class DefaultDataSourceFactory implements Factory { @Nullable private final TransferListener listener; private final DataSource.Factory baseDataSourceFactory; + /** @param context A context. */ + public DefaultDataSourceFactory(Context context) { + this(context, DEFAULT_USER_AGENT, /* listener= */ null); + } + /** * @param context A context. * @param userAgent The User-Agent string that should be used. diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultHttpDataSource.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultHttpDataSource.java index 66996c7540..d15804fd51 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultHttpDataSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultHttpDataSource.java @@ -23,6 +23,7 @@ import android.text.TextUtils; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.ExoPlayerLibraryInfo; import com.google.android.exoplayer2.upstream.DataSpec.HttpMethod; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Log; @@ -97,12 +98,26 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou private long bytesSkipped; private long bytesRead; - /** @param userAgent The User-Agent string that should be used. */ + /** Creates an instance. */ + public DefaultHttpDataSource() { + this( + ExoPlayerLibraryInfo.DEFAULT_USER_AGENT, + DEFAULT_CONNECT_TIMEOUT_MILLIS, + DEFAULT_READ_TIMEOUT_MILLIS); + } + + /** + * Creates an instance. + * + * @param userAgent The User-Agent string that should be used. + */ public DefaultHttpDataSource(String userAgent) { this(userAgent, DEFAULT_CONNECT_TIMEOUT_MILLIS, DEFAULT_READ_TIMEOUT_MILLIS); } /** + * Creates an instance. + * * @param userAgent The User-Agent string that should be used. * @param connectTimeoutMillis The connection timeout, in milliseconds. A timeout of zero is * interpreted as an infinite timeout. @@ -119,6 +134,8 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou } /** + * Creates an instance. + * * @param userAgent The User-Agent string that should be used. * @param connectTimeoutMillis The connection timeout, in milliseconds. A timeout of zero is * interpreted as an infinite timeout. Pass {@link #DEFAULT_CONNECT_TIMEOUT_MILLIS} to use the @@ -146,6 +163,8 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou } /** + * Creates an instance. + * * @param userAgent The User-Agent string that should be used. * @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the * predicate then a {@link HttpDataSource.InvalidContentTypeException} is thrown from {@link @@ -164,6 +183,8 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou } /** + * Creates an instance. + * * @param userAgent The User-Agent string that should be used. * @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the * predicate then a {@link HttpDataSource.InvalidContentTypeException} is thrown from {@link @@ -192,6 +213,8 @@ public class DefaultHttpDataSource extends BaseDataSource implements HttpDataSou } /** + * Creates an instance. + * * @param userAgent The User-Agent string that should be used. * @param contentTypePredicate An optional {@link Predicate}. If a content type is rejected by the * predicate then a {@link HttpDataSource.InvalidContentTypeException} is thrown from {@link diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultHttpDataSourceFactory.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultHttpDataSourceFactory.java index f5d7dbd24c..0a0650a4b1 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultHttpDataSourceFactory.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultHttpDataSourceFactory.java @@ -15,6 +15,8 @@ */ package com.google.android.exoplayer2.upstream; +import static com.google.android.exoplayer2.ExoPlayerLibraryInfo.DEFAULT_USER_AGENT; + import androidx.annotation.Nullable; import com.google.android.exoplayer2.upstream.HttpDataSource.BaseFactory; import com.google.android.exoplayer2.upstream.HttpDataSource.Factory; @@ -30,10 +32,18 @@ public final class DefaultHttpDataSourceFactory extends BaseFactory { private final boolean allowCrossProtocolRedirects; /** - * Constructs a DefaultHttpDataSourceFactory. Sets {@link - * DefaultHttpDataSource#DEFAULT_CONNECT_TIMEOUT_MILLIS} as the connection timeout, {@link - * DefaultHttpDataSource#DEFAULT_READ_TIMEOUT_MILLIS} as the read timeout and disables - * cross-protocol redirects. + * Creates an instance. Sets {@link DefaultHttpDataSource#DEFAULT_CONNECT_TIMEOUT_MILLIS} as the + * connection timeout, {@link DefaultHttpDataSource#DEFAULT_READ_TIMEOUT_MILLIS} as the read + * timeout and disables cross-protocol redirects. + */ + public DefaultHttpDataSourceFactory() { + this(DEFAULT_USER_AGENT); + } + + /** + * Creates an instance. Sets {@link DefaultHttpDataSource#DEFAULT_CONNECT_TIMEOUT_MILLIS} as the + * connection timeout, {@link DefaultHttpDataSource#DEFAULT_READ_TIMEOUT_MILLIS} as the read + * timeout and disables cross-protocol redirects. * * @param userAgent The User-Agent string that should be used. */ @@ -42,10 +52,9 @@ public final class DefaultHttpDataSourceFactory extends BaseFactory { } /** - * Constructs a DefaultHttpDataSourceFactory. Sets {@link - * DefaultHttpDataSource#DEFAULT_CONNECT_TIMEOUT_MILLIS} as the connection timeout, {@link - * DefaultHttpDataSource#DEFAULT_READ_TIMEOUT_MILLIS} as the read timeout and disables - * cross-protocol redirects. + * Creates an instance. Sets {@link DefaultHttpDataSource#DEFAULT_CONNECT_TIMEOUT_MILLIS} as the + * connection timeout, {@link DefaultHttpDataSource#DEFAULT_READ_TIMEOUT_MILLIS} as the read + * timeout and disables cross-protocol redirects. * * @param userAgent The User-Agent string that should be used. * @param listener An optional listener. diff --git a/library/core/src/test/java/com/google/android/exoplayer2/ExoPlayerTest.java b/library/core/src/test/java/com/google/android/exoplayer2/ExoPlayerTest.java index 66557753f4..4f9b36b702 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/ExoPlayerTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/ExoPlayerTest.java @@ -102,7 +102,6 @@ import com.google.android.exoplayer2.upstream.Loader; import com.google.android.exoplayer2.upstream.TransferListener; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Clock; -import com.google.android.exoplayer2.util.Util; import com.google.common.collect.ImmutableList; import java.io.IOException; import java.util.ArrayList; @@ -5552,8 +5551,7 @@ public final class ExoPlayerTest { AdsMediaSource adsMediaSource = new AdsMediaSource( new FakeMediaSource(new FakeTimeline(/* windowCount= */ 1)), - new DefaultDataSourceFactory( - context, Util.getUserAgent(context, ExoPlayerLibraryInfo.VERSION_SLASHY)), + new DefaultDataSourceFactory(context), new FakeAdsLoader(), new FakeAdViewProvider()); Exception[] exception = {null}; @@ -5590,8 +5588,7 @@ public final class ExoPlayerTest { AdsMediaSource adsMediaSource = new AdsMediaSource( mediaSource, - new DefaultDataSourceFactory( - context, Util.getUserAgent(context, ExoPlayerLibraryInfo.VERSION_SLASHY)), + new DefaultDataSourceFactory(context), new FakeAdsLoader(), new FakeAdViewProvider()); final Exception[] exception = {null}; @@ -5630,8 +5627,7 @@ public final class ExoPlayerTest { AdsMediaSource adsMediaSource = new AdsMediaSource( mediaSource, - new DefaultDataSourceFactory( - context, Util.getUserAgent(context, ExoPlayerLibraryInfo.VERSION_SLASHY)), + new DefaultDataSourceFactory(context), new FakeAdsLoader(), new FakeAdViewProvider()); final Exception[] exception = {null}; diff --git a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/amr/AmrExtractorSeekTest.java b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/amr/AmrExtractorSeekTest.java index 3d884e02dc..534cb2572f 100644 --- a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/amr/AmrExtractorSeekTest.java +++ b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/amr/AmrExtractorSeekTest.java @@ -51,7 +51,7 @@ public final class AmrExtractorSeekTest { @Before public void setUp() { dataSource = - new DefaultDataSourceFactory(ApplicationProvider.getApplicationContext(), "UserAgent") + new DefaultDataSourceFactory(ApplicationProvider.getApplicationContext()) .createDataSource(); } diff --git a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/flac/FlacExtractorSeekTest.java b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/flac/FlacExtractorSeekTest.java index e95c8cd7e8..16f92e2b4b 100644 --- a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/flac/FlacExtractorSeekTest.java +++ b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/flac/FlacExtractorSeekTest.java @@ -46,8 +46,7 @@ public class FlacExtractorSeekTest { private FlacExtractor extractor = new FlacExtractor(); private FakeExtractorOutput extractorOutput = new FakeExtractorOutput(); private DefaultDataSource dataSource = - new DefaultDataSourceFactory(ApplicationProvider.getApplicationContext(), "UserAgent") - .createDataSource(); + new DefaultDataSourceFactory(ApplicationProvider.getApplicationContext()).createDataSource(); @Test public void flacExtractorReads_seekTable_returnSeekableSeekMap() throws IOException { diff --git a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/mp3/ConstantBitrateSeekerTest.java b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/mp3/ConstantBitrateSeekerTest.java index b0250803f0..e3137a106d 100644 --- a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/mp3/ConstantBitrateSeekerTest.java +++ b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/mp3/ConstantBitrateSeekerTest.java @@ -52,7 +52,7 @@ public class ConstantBitrateSeekerTest { extractor = new Mp3Extractor(); extractorOutput = new FakeExtractorOutput(); dataSource = - new DefaultDataSourceFactory(ApplicationProvider.getApplicationContext(), "UserAgent") + new DefaultDataSourceFactory(ApplicationProvider.getApplicationContext()) .createDataSource(); } diff --git a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/mp3/IndexSeekerTest.java b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/mp3/IndexSeekerTest.java index 72f42fb601..24530c12f1 100644 --- a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/mp3/IndexSeekerTest.java +++ b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/mp3/IndexSeekerTest.java @@ -52,7 +52,7 @@ public class IndexSeekerTest { extractor = new Mp3Extractor(FLAG_ENABLE_INDEX_SEEKING); extractorOutput = new FakeExtractorOutput(); dataSource = - new DefaultDataSourceFactory(ApplicationProvider.getApplicationContext(), "UserAgent") + new DefaultDataSourceFactory(ApplicationProvider.getApplicationContext()) .createDataSource(); } diff --git a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/ts/AdtsExtractorSeekTest.java b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/ts/AdtsExtractorSeekTest.java index 07bf5dea1f..2770d4ef66 100644 --- a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/ts/AdtsExtractorSeekTest.java +++ b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/ts/AdtsExtractorSeekTest.java @@ -49,7 +49,7 @@ public final class AdtsExtractorSeekTest { @Before public void setUp() { dataSource = - new DefaultDataSourceFactory(ApplicationProvider.getApplicationContext(), "UserAgent") + new DefaultDataSourceFactory(ApplicationProvider.getApplicationContext()) .createDataSource(); } diff --git a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/ts/PsExtractorSeekTest.java b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/ts/PsExtractorSeekTest.java index 7dc787f842..d2d76d6695 100644 --- a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/ts/PsExtractorSeekTest.java +++ b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/ts/PsExtractorSeekTest.java @@ -68,7 +68,7 @@ public final class PsExtractorSeekTest { expectedTrackOutput = expectedOutput.trackOutputs.get(VIDEO_TRACK_ID); dataSource = - new DefaultDataSourceFactory(ApplicationProvider.getApplicationContext(), "UserAgent") + new DefaultDataSourceFactory(ApplicationProvider.getApplicationContext()) .createDataSource(); totalInputLength = readInputLength(); } diff --git a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/ts/TsExtractorSeekTest.java b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/ts/TsExtractorSeekTest.java index 4e710ec632..a796f3c994 100644 --- a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/ts/TsExtractorSeekTest.java +++ b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/ts/TsExtractorSeekTest.java @@ -62,7 +62,7 @@ public final class TsExtractorSeekTest { .get(AUDIO_TRACK_ID); dataSource = - new DefaultDataSourceFactory(ApplicationProvider.getApplicationContext(), "UserAgent") + new DefaultDataSourceFactory(ApplicationProvider.getApplicationContext()) .createDataSource(); } diff --git a/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashTestRunner.java b/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashTestRunner.java index 3d37d18182..b90b4ec6e3 100644 --- a/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashTestRunner.java +++ b/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashTestRunner.java @@ -258,12 +258,12 @@ import java.util.List; } @Override - protected DrmSessionManager buildDrmSessionManager(final String userAgent) { + protected DrmSessionManager buildDrmSessionManager() { if (widevineLicenseUrl == null) { return DrmSessionManager.getDummyDrmSessionManager(); } MediaDrmCallback drmCallback = - new HttpMediaDrmCallback(widevineLicenseUrl, new DefaultHttpDataSourceFactory(userAgent)); + new HttpMediaDrmCallback(widevineLicenseUrl, new DefaultHttpDataSourceFactory()); DefaultDrmSessionManager drmSessionManager = new DefaultDrmSessionManager.Builder() .setUuidAndExoMediaDrmProvider( @@ -301,13 +301,12 @@ import java.util.List; @Override protected MediaSource buildSource( HostActivity host, - String userAgent, DrmSessionManager drmSessionManager, FrameLayout overlayFrameLayout) { DataSource.Factory dataSourceFactory = this.dataSourceFactory != null ? this.dataSourceFactory - : new DefaultDataSourceFactory(host, userAgent); + : new DefaultDataSourceFactory(host); return new DashMediaSource.Factory(dataSourceFactory) .setDrmSessionManager(drmSessionManager) .setLoadErrorHandlingPolicy(new DefaultLoadErrorHandlingPolicy(MIN_LOADABLE_RETRY_COUNT)) diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoHostedTest.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoHostedTest.java index e66a30935e..5eececd88e 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoHostedTest.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoHostedTest.java @@ -128,7 +128,6 @@ public abstract class ExoHostedTest implements AnalyticsListener, HostedTest { this.surface = surface; // Build the player. trackSelector = buildTrackSelector(host); - String userAgent = "ExoPlayerPlaybackTests"; player = buildExoPlayer(host, surface, trackSelector); player.play(); player.addAnalyticsListener(this); @@ -140,10 +139,8 @@ public abstract class ExoHostedTest implements AnalyticsListener, HostedTest { pendingSchedule.start(player, trackSelector, surface, actionHandler, /* callback= */ null); pendingSchedule = null; } - DrmSessionManager drmSessionManager = buildDrmSessionManager(userAgent); - player.setMediaSource( - buildSource( - host, Util.getUserAgent(host, userAgent), drmSessionManager, overlayFrameLayout)); + DrmSessionManager drmSessionManager = buildDrmSessionManager(); + player.setMediaSource(buildSource(host, drmSessionManager, overlayFrameLayout)); player.prepare(); } @@ -232,7 +229,7 @@ public abstract class ExoHostedTest implements AnalyticsListener, HostedTest { return true; } - protected DrmSessionManager buildDrmSessionManager(String userAgent) { + protected DrmSessionManager buildDrmSessionManager() { // Do nothing. Interested subclasses may override. return DrmSessionManager.getDummyDrmSessionManager(); } @@ -256,7 +253,6 @@ public abstract class ExoHostedTest implements AnalyticsListener, HostedTest { protected abstract MediaSource buildSource( HostActivity host, - String userAgent, DrmSessionManager drmSessionManager, FrameLayout overlayFrameLayout); From 61b253bd5e0f4859b94b6ae9ecfd3ad8d253a3a0 Mon Sep 17 00:00:00 2001 From: insun Date: Wed, 9 Sep 2020 02:07:37 +0100 Subject: [PATCH 008/110] Fix not to show repeat button when its mode is NONE. PiperOrigin-RevId: 330627047 --- .../google/android/exoplayer2/ui/StyledPlayerControlView.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/StyledPlayerControlView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/StyledPlayerControlView.java index 86a802323a..97652ad01f 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/StyledPlayerControlView.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/StyledPlayerControlView.java @@ -716,6 +716,8 @@ public class StyledPlayerControlView extends FrameLayout { controlViewLayoutManager.setShowButton(shuffleButton, showShuffleButton); controlViewLayoutManager.setShowButton(subtitleButton, showSubtitleButton); controlViewLayoutManager.setShowButton(vrButton, showVrButton); + controlViewLayoutManager.setShowButton( + repeatToggleButton, repeatToggleModes != RepeatModeUtil.REPEAT_TOGGLE_MODE_NONE); addOnLayoutChangeListener(this::onLayoutChange); } From 29463cfc4f574ed033bbb5af680f3c6f7e7c6beb Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Wed, 9 Sep 2020 15:19:07 +0100 Subject: [PATCH 009/110] Set release date --- RELEASENOTES.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index eb3fcf298b..cd20e773d1 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -1,6 +1,6 @@ # Release notes -### 2.12.0 (not yet released - targeted for 2020-09-03) ### +### 2.12.0 (2020-09-03) ### * Core library: * `Player`: From 268ad81a98a110513e1a2e83d1e233b781b7e581 Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 9 Sep 2020 17:21:23 +0100 Subject: [PATCH 010/110] Fix Javadoc for DefaultDataSourceFactory constructors PiperOrigin-RevId: 330736458 --- .../upstream/DefaultDataSourceFactory.java | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultDataSourceFactory.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultDataSourceFactory.java index 3b7cfa9d49..68ce25c47f 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultDataSourceFactory.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultDataSourceFactory.java @@ -31,12 +31,18 @@ public final class DefaultDataSourceFactory implements Factory { @Nullable private final TransferListener listener; private final DataSource.Factory baseDataSourceFactory; - /** @param context A context. */ + /** + * Creates an instance. + * + * @param context A context. + */ public DefaultDataSourceFactory(Context context) { this(context, DEFAULT_USER_AGENT, /* listener= */ null); } /** + * Creates an instance. + * * @param context A context. * @param userAgent The User-Agent string that should be used. */ @@ -45,6 +51,8 @@ public final class DefaultDataSourceFactory implements Factory { } /** + * Creates an instance. + * * @param context A context. * @param userAgent The User-Agent string that should be used. * @param listener An optional listener. @@ -55,6 +63,8 @@ public final class DefaultDataSourceFactory implements Factory { } /** + * Creates an instance. + * * @param context A context. * @param baseDataSourceFactory A {@link Factory} to be used to create a base {@link DataSource} * for {@link DefaultDataSource}. @@ -65,6 +75,8 @@ public final class DefaultDataSourceFactory implements Factory { } /** + * Creates an instance. + * * @param context A context. * @param listener An optional listener. * @param baseDataSourceFactory A {@link Factory} to be used to create a base {@link DataSource} From fdb9bbb619d51108061810a92409b57a614fd65e Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Thu, 10 Sep 2020 11:31:40 +0100 Subject: [PATCH 011/110] Remove testutil dependency on Robolectric shadows Move shadow-related utils for end-to-end tests into core test. PiperOrigin-RevId: 330902696 --- .../google/android/exoplayer2/e2etest/Mp4PlaybackTest.java | 4 ++-- .../com/google/android/exoplayer2/e2etest/TsPlaybackTest.java | 4 ++-- .../android/exoplayer2/e2etest/util}/PlaybackOutput.java | 3 ++- .../exoplayer2/e2etest/util}/ShadowMediaCodecConfig.java | 2 +- .../com/google/android/exoplayer2/e2etest/util}/TeeCodec.java | 3 ++- testutils/build.gradle | 1 - 6 files changed, 9 insertions(+), 8 deletions(-) rename {testutils/src/main/java/com/google/android/exoplayer2/testutil => library/core/src/test/java/com/google/android/exoplayer2/e2etest/util}/PlaybackOutput.java (97%) rename {testutils/src/main/java/com/google/android/exoplayer2/testutil => library/core/src/test/java/com/google/android/exoplayer2/e2etest/util}/ShadowMediaCodecConfig.java (98%) rename {testutils/src/main/java/com/google/android/exoplayer2/testutil => library/core/src/test/java/com/google/android/exoplayer2/e2etest/util}/TeeCodec.java (96%) diff --git a/library/core/src/test/java/com/google/android/exoplayer2/e2etest/Mp4PlaybackTest.java b/library/core/src/test/java/com/google/android/exoplayer2/e2etest/Mp4PlaybackTest.java index 684399d845..021a1b3f54 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/e2etest/Mp4PlaybackTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/e2etest/Mp4PlaybackTest.java @@ -23,10 +23,10 @@ import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.android.exoplayer2.MediaItem; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.SimpleExoPlayer; +import com.google.android.exoplayer2.e2etest.util.PlaybackOutput; +import com.google.android.exoplayer2.e2etest.util.ShadowMediaCodecConfig; import com.google.android.exoplayer2.testutil.AutoAdvancingFakeClock; import com.google.android.exoplayer2.testutil.DumpFileAsserts; -import com.google.android.exoplayer2.testutil.PlaybackOutput; -import com.google.android.exoplayer2.testutil.ShadowMediaCodecConfig; import com.google.android.exoplayer2.testutil.TestExoPlayer; import org.junit.Rule; import org.junit.Test; diff --git a/library/core/src/test/java/com/google/android/exoplayer2/e2etest/TsPlaybackTest.java b/library/core/src/test/java/com/google/android/exoplayer2/e2etest/TsPlaybackTest.java index c78e4cfe96..8956cd5dc7 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/e2etest/TsPlaybackTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/e2etest/TsPlaybackTest.java @@ -22,10 +22,10 @@ import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.android.exoplayer2.MediaItem; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.SimpleExoPlayer; +import com.google.android.exoplayer2.e2etest.util.PlaybackOutput; +import com.google.android.exoplayer2.e2etest.util.ShadowMediaCodecConfig; import com.google.android.exoplayer2.testutil.AutoAdvancingFakeClock; import com.google.android.exoplayer2.testutil.DumpFileAsserts; -import com.google.android.exoplayer2.testutil.PlaybackOutput; -import com.google.android.exoplayer2.testutil.ShadowMediaCodecConfig; import com.google.android.exoplayer2.testutil.TestExoPlayer; import org.junit.Rule; import org.junit.Test; diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/PlaybackOutput.java b/library/core/src/test/java/com/google/android/exoplayer2/e2etest/util/PlaybackOutput.java similarity index 97% rename from testutils/src/main/java/com/google/android/exoplayer2/testutil/PlaybackOutput.java rename to library/core/src/test/java/com/google/android/exoplayer2/e2etest/util/PlaybackOutput.java index 69429709a4..f9c32d34b5 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/PlaybackOutput.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/e2etest/util/PlaybackOutput.java @@ -13,10 +13,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.google.android.exoplayer2.testutil; +package com.google.android.exoplayer2.e2etest.util; import com.google.android.exoplayer2.SimpleExoPlayer; import com.google.android.exoplayer2.metadata.Metadata; +import com.google.android.exoplayer2.testutil.Dumper; import com.google.android.exoplayer2.util.Assertions; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ShadowMediaCodecConfig.java b/library/core/src/test/java/com/google/android/exoplayer2/e2etest/util/ShadowMediaCodecConfig.java similarity index 98% rename from testutils/src/main/java/com/google/android/exoplayer2/testutil/ShadowMediaCodecConfig.java rename to library/core/src/test/java/com/google/android/exoplayer2/e2etest/util/ShadowMediaCodecConfig.java index d1b4e784b8..6d7f23107e 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ShadowMediaCodecConfig.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/e2etest/util/ShadowMediaCodecConfig.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.google.android.exoplayer2.testutil; +package com.google.android.exoplayer2.e2etest.util; import android.media.MediaCodecInfo; import android.media.MediaFormat; diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/TeeCodec.java b/library/core/src/test/java/com/google/android/exoplayer2/e2etest/util/TeeCodec.java similarity index 96% rename from testutils/src/main/java/com/google/android/exoplayer2/testutil/TeeCodec.java rename to library/core/src/test/java/com/google/android/exoplayer2/e2etest/util/TeeCodec.java index fd9b374d46..a14787e959 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/TeeCodec.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/e2etest/util/TeeCodec.java @@ -13,8 +13,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.google.android.exoplayer2.testutil; +package com.google.android.exoplayer2.e2etest.util; +import com.google.android.exoplayer2.testutil.Dumper; import com.google.android.exoplayer2.util.MimeTypes; import com.google.common.collect.ImmutableList; import java.nio.ByteBuffer; diff --git a/testutils/build.gradle b/testutils/build.gradle index 8cd443e07f..93b3acf53f 100644 --- a/testutils/build.gradle +++ b/testutils/build.gradle @@ -24,7 +24,6 @@ dependencies { compileOnly 'org.jetbrains.kotlin:kotlin-annotations-jvm:' + kotlinAnnotationsVersion implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion implementation project(modulePrefix + 'library-core') - implementation 'org.robolectric:robolectric:' + robolectricVersion testImplementation 'org.robolectric:robolectric:' + robolectricVersion } From 026697140687e81d87540252174f859992f36abc Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Thu, 10 Sep 2020 12:04:01 +0100 Subject: [PATCH 012/110] Add a setter for ad error listeners This is useful because ImaAdsLoader.getAdsLoader() can now return null (before ads have been requested), and it avoids the app needing to get an AdsManager to attach its listener. PiperOrigin-RevId: 330907051 --- .../exoplayer2/ext/ima/ImaAdsLoader.java | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java index 351ad43d2c..ed40a17510 100644 --- a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java +++ b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java @@ -124,6 +124,7 @@ public final class ImaAdsLoader private final Context context; @Nullable private ImaSdkSettings imaSdkSettings; + @Nullable private AdErrorListener adErrorListener; @Nullable private AdEventListener adEventListener; @Nullable private Set adUiElements; @Nullable private Collection companionAdSlots; @@ -165,6 +166,19 @@ public final class ImaAdsLoader return this; } + /** + * Sets a listener for ad errors that will be passed to {@link + * AdsLoader#addAdErrorListener(AdErrorListener)} and {@link + * AdsManager#addAdErrorListener(AdErrorListener)}. + * + * @param adErrorListener The ad error listener. + * @return This builder, for convenience. + */ + public Builder setAdErrorListener(AdErrorListener adErrorListener) { + this.adErrorListener = checkNotNull(adErrorListener); + return this; + } + /** * Sets a listener for ad events that will be passed to {@link * AdsManager#addAdEventListener(AdEventListener)}. @@ -316,6 +330,7 @@ public final class ImaAdsLoader playAdBeforeStartPosition, adUiElements, companionAdSlots, + adErrorListener, adEventListener, imaFactory); } @@ -341,6 +356,7 @@ public final class ImaAdsLoader playAdBeforeStartPosition, adUiElements, companionAdSlots, + adErrorListener, adEventListener, imaFactory); } @@ -408,6 +424,7 @@ public final class ImaAdsLoader private final int mediaBitrate; @Nullable private final Set adUiElements; @Nullable private final Collection companionAdSlots; + @Nullable private final AdErrorListener adErrorListener; @Nullable private final AdEventListener adEventListener; private final ImaFactory imaFactory; private final ImaSdkSettings imaSdkSettings; @@ -516,6 +533,7 @@ public final class ImaAdsLoader /* playAdBeforeStartPosition= */ true, /* adUiElements= */ null, /* companionAdSlots= */ null, + /* adErrorListener= */ null, /* adEventListener= */ null, /* imaFactory= */ new DefaultImaFactory()); } @@ -534,6 +552,7 @@ public final class ImaAdsLoader boolean playAdBeforeStartPosition, @Nullable Set adUiElements, @Nullable Collection companionAdSlots, + @Nullable AdErrorListener adErrorListener, @Nullable AdEventListener adEventListener, ImaFactory imaFactory) { checkArgument(adTagUri != null || adsResponse != null); @@ -548,6 +567,7 @@ public final class ImaAdsLoader this.playAdBeforeStartPosition = playAdBeforeStartPosition; this.adUiElements = adUiElements; this.companionAdSlots = companionAdSlots; + this.adErrorListener = adErrorListener; this.adEventListener = adEventListener; this.imaFactory = imaFactory; if (imaSdkSettings == null) { @@ -629,6 +649,9 @@ public final class ImaAdsLoader } adsLoader = imaFactory.createAdsLoader(context, imaSdkSettings, adDisplayContainer); adsLoader.addAdErrorListener(componentListener); + if (adErrorListener != null) { + adsLoader.addAdErrorListener(adErrorListener); + } adsLoader.addAdsLoadedListener(componentListener); AdsRequest request = imaFactory.createAdsRequest(); if (adTagUri != null) { @@ -759,6 +782,9 @@ public final class ImaAdsLoader if (adsLoader != null) { adsLoader.removeAdsLoadedListener(componentListener); adsLoader.removeAdErrorListener(componentListener); + if (adErrorListener != null) { + adsLoader.removeAdErrorListener(adErrorListener); + } } imaPausedContent = false; imaAdState = IMA_AD_STATE_NONE; @@ -1582,6 +1608,9 @@ public final class ImaAdsLoader private void destroyAdsManager() { if (adsManager != null) { adsManager.removeAdErrorListener(componentListener); + if (adErrorListener != null) { + adsManager.removeAdErrorListener(adErrorListener); + } adsManager.removeAdEventListener(componentListener); if (adEventListener != null) { adsManager.removeAdEventListener(adEventListener); @@ -1642,6 +1671,9 @@ public final class ImaAdsLoader pendingAdRequestContext = null; ImaAdsLoader.this.adsManager = adsManager; adsManager.addAdErrorListener(this); + if (adErrorListener != null) { + adsManager.addAdErrorListener(adErrorListener); + } adsManager.addAdEventListener(this); if (adEventListener != null) { adsManager.addAdEventListener(adEventListener); From 4d2a2384d685b09385ace1f2161088e5d9dfdba2 Mon Sep 17 00:00:00 2001 From: tonihei Date: Thu, 10 Sep 2020 12:12:23 +0100 Subject: [PATCH 013/110] Add convenience constructor methods. When passing in ExtractorFactory instances to SimpleExoPlayer.Builder or DefaultMediaSourceFactory, we currently need to pass in one other instance (RenderersFactory or DataSource.Factory), that developers will often set to its default. To avoid specifying these defaults, these new convience methods allow to just set the ExtractorsFactory if required. PiperOrigin-RevId: 330908002 --- .../android/exoplayer2/SimpleExoPlayer.java | 31 ++++++++++++------- .../source/DefaultMediaSourceFactory.java | 16 ++++++++-- 2 files changed, 33 insertions(+), 14 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java b/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java index ba3a375a37..39490d5709 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java @@ -54,7 +54,6 @@ import com.google.android.exoplayer2.trackselection.TrackSelectionArray; import com.google.android.exoplayer2.trackselection.TrackSelector; import com.google.android.exoplayer2.upstream.BandwidthMeter; import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter; -import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Clock; import com.google.android.exoplayer2.util.Log; @@ -118,11 +117,11 @@ public class SimpleExoPlayer extends BasePlayer /** * Creates a builder. * - *

    Use {@link #Builder(Context, RenderersFactory)} or {@link #Builder(Context, - * RenderersFactory, ExtractorsFactory)} instead, if you intend to provide a custom {@link - * RenderersFactory} or a custom {@link ExtractorsFactory}. This is to ensure that ProGuard or - * R8 can remove ExoPlayer's {@link DefaultRenderersFactory} and {@link - * DefaultExtractorsFactory} from the APK. + *

    Use {@link #Builder(Context, RenderersFactory)}, {@link #Builder(Context, + * RenderersFactory)} or {@link #Builder(Context, RenderersFactory, ExtractorsFactory)} instead, + * if you intend to provide a custom {@link RenderersFactory} or a custom {@link + * ExtractorsFactory}. This is to ensure that ProGuard or R8 can remove ExoPlayer's {@link + * DefaultRenderersFactory} and {@link DefaultExtractorsFactory} from the APK. * *

    The builder uses the following default values: * @@ -167,6 +166,19 @@ public class SimpleExoPlayer extends BasePlayer this(context, renderersFactory, new DefaultExtractorsFactory()); } + /** + * Creates a builder with a custom {@link ExtractorsFactory}. + * + *

    See {@link #Builder(Context)} for a list of default values. + * + * @param context A {@link Context}. + * @param extractorsFactory An {@link ExtractorsFactory} used to extract progressive media from + * its container. + */ + public Builder(Context context, ExtractorsFactory extractorsFactory) { + this(context, new DefaultRenderersFactory(context), extractorsFactory); + } + /** * Creates a builder with a custom {@link RenderersFactory} and {@link ExtractorsFactory}. * @@ -184,10 +196,7 @@ public class SimpleExoPlayer extends BasePlayer context, renderersFactory, new DefaultTrackSelector(context), - new DefaultMediaSourceFactory( - new DefaultDataSourceFactory( - context, Util.getUserAgent(context, ExoPlayerLibraryInfo.VERSION_SLASHY)), - extractorsFactory), + new DefaultMediaSourceFactory(context, extractorsFactory), new DefaultLoadControl(), DefaultBandwidthMeter.getSingletonInstance(context), new AnalyticsCollector(Clock.DEFAULT)); @@ -570,7 +579,7 @@ public class SimpleExoPlayer extends BasePlayer Clock clock, Looper applicationLooper) { this( - new Builder(context, renderersFactory, new DefaultExtractorsFactory()) + new Builder(context, renderersFactory) .setTrackSelector(trackSelector) .setMediaSourceFactory(mediaSourceFactory) .setLoadControl(loadControl) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/DefaultMediaSourceFactory.java b/library/core/src/main/java/com/google/android/exoplayer2/source/DefaultMediaSourceFactory.java index f4fc7e4afe..3f1c03d3b1 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/DefaultMediaSourceFactory.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/DefaultMediaSourceFactory.java @@ -65,9 +65,8 @@ import java.util.List; * MediaItem.PlaybackProperties#uri uri} doesn't match one of the above. It tries to infer the * required extractor by using the {@link * com.google.android.exoplayer2.extractor.DefaultExtractorsFactory} or the {@link - * ExtractorsFactory} provided in {@link #DefaultMediaSourceFactory(DataSource.Factory, - * ExtractorsFactory)}. An {@link UnrecognizedInputFormatException} is thrown if none of the - * available extractors can read the stream. + * ExtractorsFactory} provided in the constructor. An {@link UnrecognizedInputFormatException} + * is thrown if none of the available extractors can read the stream. * * *

    Ad support for media items with ad tag URIs

    @@ -117,6 +116,17 @@ public final class DefaultMediaSourceFactory implements MediaSourceFactory { this(new DefaultDataSourceFactory(context)); } + /** + * Creates a new instance. + * + * @param context Any context. + * @param extractorsFactory An {@link ExtractorsFactory} used to extract progressive media from + * its container. + */ + public DefaultMediaSourceFactory(Context context, ExtractorsFactory extractorsFactory) { + this(new DefaultDataSourceFactory(context), extractorsFactory); + } + /** * Creates a new instance. * From 52a1c791f7ecdaae9fc8f6cd647108eae677eb95 Mon Sep 17 00:00:00 2001 From: krocard Date: Thu, 10 Sep 2020 13:49:56 +0100 Subject: [PATCH 014/110] Introduce audio offload scheduling tests PiperOrigin-RevId: 330918396 --- .../android/exoplayer2/ExoPlayerTest.java | 162 ++++++++++++++++++ .../exoplayer2/testutil/TestExoPlayer.java | 28 +++ 2 files changed, 190 insertions(+) diff --git a/library/core/src/test/java/com/google/android/exoplayer2/ExoPlayerTest.java b/library/core/src/test/java/com/google/android/exoplayer2/ExoPlayerTest.java index 4f9b36b702..b8f10a2dc2 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/ExoPlayerTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/ExoPlayerTest.java @@ -19,6 +19,7 @@ import static com.google.android.exoplayer2.testutil.FakeSampleStream.FakeSample import static com.google.android.exoplayer2.testutil.FakeSampleStream.FakeSampleStreamItem.oneByteSample; import static com.google.android.exoplayer2.testutil.TestExoPlayer.playUntilStartOfWindow; import static com.google.android.exoplayer2.testutil.TestExoPlayer.runUntilPlaybackState; +import static com.google.android.exoplayer2.testutil.TestExoPlayer.runUntilReceiveOffloadSchedulingEnabledNewState; import static com.google.android.exoplayer2.testutil.TestExoPlayer.runUntilTimelineChanged; import static com.google.android.exoplayer2.testutil.TestUtil.runMainLooperUntil; import static com.google.common.truth.Truth.assertThat; @@ -110,6 +111,7 @@ import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicLong; @@ -8208,6 +8210,109 @@ public final class ExoPlayerTest { assertThat(player.getCurrentWindowIndex()).isEqualTo(0); } + @Test + public void enableOffloadSchedulingWhileIdle_isToggled_isReported() throws Exception { + SimpleExoPlayer player = new TestExoPlayer.Builder(context).build(); + + player.experimentalSetOffloadSchedulingEnabled(true); + assertThat(runUntilReceiveOffloadSchedulingEnabledNewState(player)).isTrue(); + + player.experimentalSetOffloadSchedulingEnabled(false); + assertThat(runUntilReceiveOffloadSchedulingEnabledNewState(player)).isFalse(); + } + + @Test + public void enableOffloadSchedulingWhilePlaying_isToggled_isReported() throws Exception { + FakeSleepRenderer sleepRenderer = new FakeSleepRenderer(C.TRACK_TYPE_AUDIO).sleepOnNextRender(); + SimpleExoPlayer player = new TestExoPlayer.Builder(context).setRenderers(sleepRenderer).build(); + Timeline timeline = new FakeTimeline(/* windowCount= */ 1); + player.setMediaSource(new FakeMediaSource(timeline, ExoPlayerTestRunner.AUDIO_FORMAT)); + player.prepare(); + player.play(); + + player.experimentalSetOffloadSchedulingEnabled(true); + assertThat(runUntilReceiveOffloadSchedulingEnabledNewState(player)).isTrue(); + + player.experimentalSetOffloadSchedulingEnabled(false); + assertThat(runUntilReceiveOffloadSchedulingEnabledNewState(player)).isFalse(); + } + + @Test + public void enableOffloadSchedulingWhileSleepingForOffload_isDisabled_isReported() + throws Exception { + FakeSleepRenderer sleepRenderer = new FakeSleepRenderer(C.TRACK_TYPE_AUDIO).sleepOnNextRender(); + SimpleExoPlayer player = new TestExoPlayer.Builder(context).setRenderers(sleepRenderer).build(); + Timeline timeline = new FakeTimeline(/* windowCount= */ 1); + player.setMediaSource(new FakeMediaSource(timeline, ExoPlayerTestRunner.AUDIO_FORMAT)); + player.experimentalSetOffloadSchedulingEnabled(true); + runUntilReceiveOffloadSchedulingEnabledNewState(player); + player.prepare(); + player.play(); + runMainLooperUntil(sleepRenderer::isSleeping); + + player.experimentalSetOffloadSchedulingEnabled(false); + + assertThat(runUntilReceiveOffloadSchedulingEnabledNewState(player)).isFalse(); + } + + @Test + public void enableOffloadScheduling_isEnable_playerSleeps() throws Exception { + FakeSleepRenderer sleepRenderer = new FakeSleepRenderer(C.TRACK_TYPE_AUDIO); + SimpleExoPlayer player = new TestExoPlayer.Builder(context).setRenderers(sleepRenderer).build(); + Timeline timeline = new FakeTimeline(/* windowCount= */ 1); + player.setMediaSource(new FakeMediaSource(timeline, ExoPlayerTestRunner.AUDIO_FORMAT)); + player.experimentalSetOffloadSchedulingEnabled(true); + player.prepare(); + player.play(); + + sleepRenderer.sleepOnNextRender(); + + runMainLooperUntil(sleepRenderer::isSleeping); + // TODO(b/163303129): There is currently no way to check that the player is sleeping for + // offload, for now use a timeout to check that the renderer is never woken up. + final int renderTimeoutMs = 500; + assertThrows( + TimeoutException.class, + () -> + runMainLooperUntil(() -> !sleepRenderer.isSleeping(), renderTimeoutMs, Clock.DEFAULT)); + } + + @Test + public void + experimentalEnableOffloadSchedulingWhileSleepingForOffload_isDisabled_renderingResumes() + throws Exception { + FakeSleepRenderer sleepRenderer = new FakeSleepRenderer(C.TRACK_TYPE_AUDIO).sleepOnNextRender(); + SimpleExoPlayer player = new TestExoPlayer.Builder(context).setRenderers(sleepRenderer).build(); + Timeline timeline = new FakeTimeline(/* windowCount= */ 1); + player.setMediaSource(new FakeMediaSource(timeline, ExoPlayerTestRunner.AUDIO_FORMAT)); + player.experimentalSetOffloadSchedulingEnabled(true); + player.prepare(); + player.play(); + runMainLooperUntil(sleepRenderer::isSleeping); + + player.experimentalSetOffloadSchedulingEnabled(false); // Force the player to exit offload sleep + + runMainLooperUntil(() -> !sleepRenderer.isSleeping()); + runUntilPlaybackState(player, Player.STATE_ENDED); + } + + @Test + public void wakeupListenerWhileSleepingForOffload_isWokenUp_renderingResumes() throws Exception { + FakeSleepRenderer sleepRenderer = new FakeSleepRenderer(C.TRACK_TYPE_AUDIO).sleepOnNextRender(); + SimpleExoPlayer player = new TestExoPlayer.Builder(context).setRenderers(sleepRenderer).build(); + Timeline timeline = new FakeTimeline(/* windowCount= */ 1); + player.setMediaSource(new FakeMediaSource(timeline, ExoPlayerTestRunner.AUDIO_FORMAT)); + player.experimentalSetOffloadSchedulingEnabled(true); + player.prepare(); + player.play(); + runMainLooperUntil(sleepRenderer::isSleeping); + + sleepRenderer.wakeup(); + + runMainLooperUntil(() -> !sleepRenderer.isSleeping()); + runUntilPlaybackState(player, Player.STATE_ENDED); + } + // Internal methods. private static ActionSchedule.Builder addSurfaceSwitch(ActionSchedule.Builder builder) { @@ -8237,6 +8342,63 @@ public final class ExoPlayerTest { // Internal classes. + /* {@link FakeRenderer} that can sleep and be woken-up. */ + private static class FakeSleepRenderer extends FakeRenderer { + private static final long WAKEUP_DEADLINE_MS = 60 * C.MICROS_PER_SECOND; + private final AtomicBoolean sleepOnNextRender; + private final AtomicBoolean isSleeping; + private final AtomicReference wakeupListenerReceiver; + + public FakeSleepRenderer(int trackType) { + super(trackType); + sleepOnNextRender = new AtomicBoolean(false); + isSleeping = new AtomicBoolean(false); + wakeupListenerReceiver = new AtomicReference<>(); + } + + public void wakeup() { + wakeupListenerReceiver.get().onWakeup(); + } + + /** + * Call {@link Renderer.WakeupListener#onSleep(long)} on the next {@link #render(long, long)} + */ + public FakeSleepRenderer sleepOnNextRender() { + sleepOnNextRender.set(true); + return this; + } + + /** + * Returns whether {@link Renderer.WakeupListener#onSleep(long)} was called on the last {@link + * #render(long, long)} + */ + public boolean isSleeping() { + return isSleeping.get(); + } + + @Override + public void handleMessage(int what, @Nullable Object object) throws ExoPlaybackException { + if (what == MSG_SET_WAKEUP_LISTENER) { + assertThat(object).isNotNull(); + wakeupListenerReceiver.set((WakeupListener) object); + } + super.handleMessage(what, object); + } + + @Override + public void render(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackException { + super.render(positionUs, elapsedRealtimeUs); + if (sleepOnNextRender.compareAndSet(/* expect= */ true, /* update= */ false)) { + wakeupListenerReceiver.get().onSleep(WAKEUP_DEADLINE_MS); + // TODO(b/163303129): Use an actual message from the player instead of guessing that the + // player will always sleep for offload after calling `onSleep`. + isSleeping.set(true); + } else { + isSleeping.set(false); + } + } + } + private static final class CountingMessageTarget implements PlayerMessage.Target { public int messageCount; diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/TestExoPlayer.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/TestExoPlayer.java index 6feea08a02..6b8f32ef01 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/TestExoPlayer.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/TestExoPlayer.java @@ -44,6 +44,7 @@ import com.google.android.exoplayer2.video.VideoListener; import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; +import org.checkerframework.checker.nullness.compatqual.NullableType; import org.checkerframework.checker.nullness.qual.MonotonicNonNull; /** @@ -438,6 +439,33 @@ public class TestExoPlayer { return receivedError.get(); } + /** + * Runs tasks of the main {@link Looper} until a {@link + * Player.EventListener#onExperimentalOffloadSchedulingEnabledChanged} callback occurred. + * + * @param player The {@link Player}. + * @return The new offloadSchedulingEnabled state. + * @throws TimeoutException If the {@link TestUtil#DEFAULT_TIMEOUT_MS default timeout} is + * exceeded. + */ + public static boolean runUntilReceiveOffloadSchedulingEnabledNewState(Player player) + throws TimeoutException { + verifyMainTestThread(player); + AtomicReference<@NullableType Boolean> offloadSchedulingEnabledReceiver = + new AtomicReference<>(); + Player.EventListener listener = + new Player.EventListener() { + @Override + public void onExperimentalOffloadSchedulingEnabledChanged( + boolean offloadSchedulingEnabled) { + offloadSchedulingEnabledReceiver.set(offloadSchedulingEnabled); + } + }; + player.addListener(listener); + runMainLooperUntil(() -> offloadSchedulingEnabledReceiver.get() != null); + return Assertions.checkNotNull(offloadSchedulingEnabledReceiver.get()); + } + /** * Runs tasks of the main {@link Looper} until the {@link VideoListener#onRenderedFirstFrame} * callback has been called. From a67bebc1c94cf5bdcd62819aa2ea4ebfb00ef53c Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Thu, 10 Sep 2020 13:52:23 +0100 Subject: [PATCH 015/110] Improve DEBUG VideoProgressUpdate logging PiperOrigin-RevId: 330918689 --- .../android/exoplayer2/ext/ima/ImaAdsLoader.java | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java index ed40a17510..a5539248b6 100644 --- a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java +++ b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java @@ -1696,7 +1696,15 @@ public final class ImaAdsLoader public VideoProgressUpdate getContentProgress() { VideoProgressUpdate videoProgressUpdate = getContentVideoProgressUpdate(); if (DEBUG) { - Log.d(TAG, "Content progress: " + videoProgressUpdate); + if (VideoProgressUpdate.VIDEO_TIME_NOT_READY.equals(videoProgressUpdate)) { + Log.d(TAG, "Content progress: not ready"); + } else { + Log.d( + TAG, + Util.formatInvariant( + "Content progress: %.1f of %.1f s", + videoProgressUpdate.getCurrentTime(), videoProgressUpdate.getDuration())); + } } if (waitingForPreloadElapsedRealtimeMs != C.TIME_UNSET) { From c75077cb9e60f15e9a9ed70ad92b6a01e902a425 Mon Sep 17 00:00:00 2001 From: tonihei Date: Thu, 10 Sep 2020 18:01:01 +0100 Subject: [PATCH 016/110] Release player in e2e playback tests. Not releasing the player means the playback thread keeps running and also keeps its entire allocated playback buffer. PiperOrigin-RevId: 330958821 --- .../com/google/android/exoplayer2/e2etest/Mp4PlaybackTest.java | 1 + .../com/google/android/exoplayer2/e2etest/TsPlaybackTest.java | 1 + 2 files changed, 2 insertions(+) diff --git a/library/core/src/test/java/com/google/android/exoplayer2/e2etest/Mp4PlaybackTest.java b/library/core/src/test/java/com/google/android/exoplayer2/e2etest/Mp4PlaybackTest.java index 021a1b3f54..f37610d982 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/e2etest/Mp4PlaybackTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/e2etest/Mp4PlaybackTest.java @@ -55,6 +55,7 @@ public class Mp4PlaybackTest { player.prepare(); player.play(); TestExoPlayer.runUntilPlaybackState(player, Player.STATE_ENDED); + player.release(); DumpFileAsserts.assertOutput( ApplicationProvider.getApplicationContext(), diff --git a/library/core/src/test/java/com/google/android/exoplayer2/e2etest/TsPlaybackTest.java b/library/core/src/test/java/com/google/android/exoplayer2/e2etest/TsPlaybackTest.java index 8956cd5dc7..d57f06ff52 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/e2etest/TsPlaybackTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/e2etest/TsPlaybackTest.java @@ -55,6 +55,7 @@ public class TsPlaybackTest { player.prepare(); player.play(); TestExoPlayer.runUntilPlaybackState(player, Player.STATE_ENDED); + player.release(); DumpFileAsserts.assertOutput( ApplicationProvider.getApplicationContext(), From 4648a4196273c0938cf84ffa569ba025edd2d97e Mon Sep 17 00:00:00 2001 From: tonihei Date: Thu, 10 Sep 2020 18:10:28 +0100 Subject: [PATCH 017/110] Don't keep 100MB static buffer in test. This may remove available memory from other tests running in the same process. Instead, create the huge buffer when needed so it can be GCed immediately. PiperOrigin-RevId: 330960844 --- .../android/exoplayer2/mediacodec/BatchBufferTest.java | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/library/core/src/test/java/com/google/android/exoplayer2/mediacodec/BatchBufferTest.java b/library/core/src/test/java/com/google/android/exoplayer2/mediacodec/BatchBufferTest.java index a48dcf7945..d281b363ca 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/mediacodec/BatchBufferTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/mediacodec/BatchBufferTest.java @@ -39,8 +39,6 @@ public final class BatchBufferTest { private static final byte[] TEST_ACCESS_UNIT = TestUtil.buildTestData(BUFFER_SIZE_MUCH_SMALLER_THAN_BATCH_SIZE_BYTES); - private static final byte[] TEST_HUGE_ACCESS_UNIT = - TestUtil.buildTestData(BUFFER_SIZE_LARGER_THAN_BATCH_SIZE_BYTES); private final BatchBuffer batchBuffer = new BatchBuffer(); @@ -163,15 +161,16 @@ public final class BatchBufferTest { batchBuffer.getNextAccessUnitBuffer().ensureSpaceForWrite(TEST_ACCESS_UNIT.length); batchBuffer.getNextAccessUnitBuffer().data.put(TEST_ACCESS_UNIT); batchBuffer.commitNextAccessUnit(); - batchBuffer.getNextAccessUnitBuffer().ensureSpaceForWrite(TEST_HUGE_ACCESS_UNIT.length); - batchBuffer.getNextAccessUnitBuffer().data.put(TEST_HUGE_ACCESS_UNIT); + byte[] hugeAccessUnit = TestUtil.buildTestData(BUFFER_SIZE_LARGER_THAN_BATCH_SIZE_BYTES); + batchBuffer.getNextAccessUnitBuffer().ensureSpaceForWrite(hugeAccessUnit.length); + batchBuffer.getNextAccessUnitBuffer().data.put(hugeAccessUnit); batchBuffer.commitNextAccessUnit(); batchBuffer.batchWasConsumed(); batchBuffer.flip(); assertThat(batchBuffer.getAccessUnitCount()).isEqualTo(1); - assertThat(batchBuffer.data).isEqualTo(ByteBuffer.wrap(TEST_HUGE_ACCESS_UNIT)); + assertThat(batchBuffer.data).isEqualTo(ByteBuffer.wrap(hugeAccessUnit)); } @Test From 862cf26534d5050a6706f341f97fce3da2b4a06a Mon Sep 17 00:00:00 2001 From: bachinger Date: Thu, 10 Sep 2020 23:08:13 +0100 Subject: [PATCH 018/110] fix typo PiperOrigin-RevId: 331025924 --- .../java/com/google/android/exoplayer2/SimpleExoPlayer.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java b/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java index 39490d5709..6652cbb03d 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/SimpleExoPlayer.java @@ -345,7 +345,7 @@ public class SimpleExoPlayer extends BasePlayer * IllegalArgumentException}. * * @param audioAttributes {@link AudioAttributes}. - * @param handleAudioFocus Whether the player should hanlde audio focus. + * @param handleAudioFocus Whether the player should handle audio focus. * @return This builder. * @throws IllegalStateException If {@link #build()} has already been called. */ From 5bd61116a252b5c0fb82d70ec45902cb7b6773e6 Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 10 Sep 2020 23:17:17 +0100 Subject: [PATCH 019/110] Fix incorrect type when creating ExoPlaybackException PiperOrigin-RevId: 331027732 --- .../com/google/android/exoplayer2/ExoPlaybackException.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlaybackException.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlaybackException.java index 4f22bbbe28..93fb4b0118 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlaybackException.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlaybackException.java @@ -218,7 +218,7 @@ public final class ExoPlaybackException extends Exception { public static ExoPlaybackException createForTimeout( TimeoutException cause, @TimeoutOperation int timeoutOperation) { return new ExoPlaybackException( - TYPE_OUT_OF_MEMORY, + TYPE_TIMEOUT, cause, /* customMessage= */ null, /* rendererName= */ null, From 4691aa84153ad3fbb837724c906e6941aba181b6 Mon Sep 17 00:00:00 2001 From: ibaker Date: Fri, 11 Sep 2020 11:57:50 +0100 Subject: [PATCH 020/110] Throw RuntimeException instead of Error from ExoHostedTest Throwing Error forces a test to catch Throwable (e.g. DashWidevineOfflineTest#widevineOfflineReleasedV22), which will also catch AssertionError meaning the fail() call at the end of the try block won't work. The DashWidevineOfflineTest have been broken since https://github.com/google/ExoPlayer/commit/91185500a1242b99b86b18bc9f3449d3dac1fa01 PiperOrigin-RevId: 331120894 --- .../com/google/android/exoplayer2/testutil/ExoHostedTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoHostedTest.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoHostedTest.java index 5eececd88e..fbdd9590aa 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoHostedTest.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/ExoHostedTest.java @@ -157,7 +157,7 @@ public abstract class ExoHostedTest implements AnalyticsListener, HostedTest { @Override public final void onFinished() { if (failOnPlayerError && playerError != null) { - throw new Error(playerError); + throw new RuntimeException(playerError); } logMetrics(audioDecoderCounters, videoDecoderCounters); if (expectedPlayingTimeMs != EXPECTED_PLAYING_TIME_UNSET) { From ea25729a092f478005fe028ce2ec314685c31730 Mon Sep 17 00:00:00 2001 From: olly Date: Fri, 11 Sep 2020 12:28:00 +0100 Subject: [PATCH 021/110] Make BatchBufferTest allocate less memory Setting to 2x BATCH_SIZE_BYTES PiperOrigin-RevId: 331124129 --- .../google/android/exoplayer2/mediacodec/BatchBufferTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/library/core/src/test/java/com/google/android/exoplayer2/mediacodec/BatchBufferTest.java b/library/core/src/test/java/com/google/android/exoplayer2/mediacodec/BatchBufferTest.java index d281b363ca..6579e8ee06 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/mediacodec/BatchBufferTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/mediacodec/BatchBufferTest.java @@ -33,7 +33,7 @@ import org.junit.runner.RunWith; public final class BatchBufferTest { /** Bigger than {@code BatchBuffer.BATCH_SIZE_BYTES} */ - private static final int BUFFER_SIZE_LARGER_THAN_BATCH_SIZE_BYTES = 100 * 1000 * 1000; + private static final int BUFFER_SIZE_LARGER_THAN_BATCH_SIZE_BYTES = 6 * 1000 * 1024; /** Smaller than {@code BatchBuffer.BATCH_SIZE_BYTES} */ private static final int BUFFER_SIZE_MUCH_SMALLER_THAN_BATCH_SIZE_BYTES = 100; From 9fb2902dada11adb4dea5f1c16e87cc083082216 Mon Sep 17 00:00:00 2001 From: olly Date: Fri, 11 Sep 2020 15:59:20 +0100 Subject: [PATCH 022/110] Exclude Guava transitive annotation dependencies PiperOrigin-RevId: 331148067 --- demos/main/build.gradle | 8 +++++++- extensions/cronet/build.gradle | 8 +++++++- extensions/ima/build.gradle | 24 +++++++++++++++++++++--- extensions/media2/build.gradle | 8 +++++++- extensions/okhttp/build.gradle | 8 +++++++- extensions/workmanager/build.gradle | 8 +++++++- library/common/build.gradle | 8 +++++++- library/core/build.gradle | 24 +++++++++++++++++++++--- library/dash/build.gradle | 8 +++++++- library/extractor/build.gradle | 8 +++++++- library/hls/build.gradle | 8 +++++++- library/ui/build.gradle | 8 +++++++- 12 files changed, 112 insertions(+), 16 deletions(-) diff --git a/demos/main/build.gradle b/demos/main/build.gradle index 0c628be879..3a3b7a4a45 100644 --- a/demos/main/build.gradle +++ b/demos/main/build.gradle @@ -70,7 +70,13 @@ dependencies { implementation 'androidx.appcompat:appcompat:' + androidxAppCompatVersion implementation 'androidx.multidex:multidex:' + androidxMultidexVersion implementation 'com.google.android.material:material:1.2.0' - implementation 'com.google.guava:guava:' + guavaVersion + implementation ('com.google.guava:guava:' + guavaVersion) { + exclude group: 'com.google.code.findbugs', module: 'jsr305' + exclude group: 'org.checkerframework', module: 'checker-compat-qual' + exclude group: 'com.google.errorprone', module: 'error_prone_annotations' + exclude group: 'com.google.j2objc', module: 'j2objc-annotations' + exclude group: 'org.codehaus.mojo', module: 'animal-sniffer-annotations' + } implementation project(modulePrefix + 'library-core') implementation project(modulePrefix + 'library-dash') implementation project(modulePrefix + 'library-hls') diff --git a/extensions/cronet/build.gradle b/extensions/cronet/build.gradle index ed8f4f5926..0dd1d42d72 100644 --- a/extensions/cronet/build.gradle +++ b/extensions/cronet/build.gradle @@ -17,7 +17,13 @@ dependencies { api "com.google.android.gms:play-services-cronet:17.0.0" implementation project(modulePrefix + 'library-core') implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion - implementation 'com.google.guava:guava:' + guavaVersion + implementation ('com.google.guava:guava:' + guavaVersion) { + exclude group: 'com.google.code.findbugs', module: 'jsr305' + exclude group: 'org.checkerframework', module: 'checker-compat-qual' + exclude group: 'com.google.errorprone', module: 'error_prone_annotations' + exclude group: 'com.google.j2objc', module: 'j2objc-annotations' + exclude group: 'org.codehaus.mojo', module: 'animal-sniffer-annotations' + } compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion compileOnly 'org.jetbrains.kotlin:kotlin-annotations-jvm:' + kotlinAnnotationsVersion testImplementation project(modulePrefix + 'library') diff --git a/extensions/ima/build.gradle b/extensions/ima/build.gradle index ee7ceec6db..f7b2b3f77c 100644 --- a/extensions/ima/build.gradle +++ b/extensions/ima/build.gradle @@ -29,17 +29,35 @@ dependencies { implementation project(modulePrefix + 'library-core') implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion implementation 'com.google.android.gms:play-services-ads-identifier:17.0.0' - implementation 'com.google.guava:guava:' + guavaVersion + implementation ('com.google.guava:guava:' + guavaVersion) { + exclude group: 'com.google.code.findbugs', module: 'jsr305' + exclude group: 'org.checkerframework', module: 'checker-compat-qual' + exclude group: 'com.google.errorprone', module: 'error_prone_annotations' + exclude group: 'com.google.j2objc', module: 'j2objc-annotations' + exclude group: 'org.codehaus.mojo', module: 'animal-sniffer-annotations' + } compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion compileOnly 'org.jetbrains.kotlin:kotlin-annotations-jvm:' + kotlinAnnotationsVersion androidTestImplementation project(modulePrefix + 'testutils') androidTestImplementation 'androidx.multidex:multidex:' + androidxMultidexVersion androidTestImplementation 'androidx.test:rules:' + androidxTestRulesVersion androidTestImplementation 'androidx.test:runner:' + androidxTestRunnerVersion - androidTestImplementation 'com.google.guava:guava:' + guavaVersion + androidTestImplementation ('com.google.guava:guava:' + guavaVersion) { + exclude group: 'com.google.code.findbugs', module: 'jsr305' + exclude group: 'org.checkerframework', module: 'checker-compat-qual' + exclude group: 'com.google.errorprone', module: 'error_prone_annotations' + exclude group: 'com.google.j2objc', module: 'j2objc-annotations' + exclude group: 'org.codehaus.mojo', module: 'animal-sniffer-annotations' + } androidTestCompileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion testImplementation project(modulePrefix + 'testutils') - testImplementation 'com.google.guava:guava:' + guavaVersion + testImplementation ('com.google.guava:guava:' + guavaVersion) { + exclude group: 'com.google.code.findbugs', module: 'jsr305' + exclude group: 'org.checkerframework', module: 'checker-compat-qual' + exclude group: 'com.google.errorprone', module: 'error_prone_annotations' + exclude group: 'com.google.j2objc', module: 'j2objc-annotations' + exclude group: 'org.codehaus.mojo', module: 'animal-sniffer-annotations' + } testImplementation 'org.robolectric:robolectric:' + robolectricVersion } diff --git a/extensions/media2/build.gradle b/extensions/media2/build.gradle index 5c52cc2f33..cedb4cd55f 100644 --- a/extensions/media2/build.gradle +++ b/extensions/media2/build.gradle @@ -19,7 +19,13 @@ dependencies { implementation project(modulePrefix + 'library-core') implementation 'androidx.collection:collection:' + androidxCollectionVersion implementation 'androidx.concurrent:concurrent-futures:1.0.0' - implementation 'com.google.guava:guava:' + guavaVersion + implementation ('com.google.guava:guava:' + guavaVersion) { + exclude group: 'com.google.code.findbugs', module: 'jsr305' + exclude group: 'org.checkerframework', module: 'checker-compat-qual' + exclude group: 'com.google.errorprone', module: 'error_prone_annotations' + exclude group: 'com.google.j2objc', module: 'j2objc-annotations' + exclude group: 'org.codehaus.mojo', module: 'animal-sniffer-annotations' + } api 'androidx.media2:media2-session:1.0.3' compileOnly 'org.checkerframework:checker-compat-qual:' + checkerframeworkCompatVersion compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion diff --git a/extensions/okhttp/build.gradle b/extensions/okhttp/build.gradle index 217bfa76cd..f16e382aa1 100644 --- a/extensions/okhttp/build.gradle +++ b/extensions/okhttp/build.gradle @@ -16,7 +16,13 @@ apply from: "$gradle.ext.exoplayerSettingsDir/common_library_config.gradle" dependencies { implementation project(modulePrefix + 'library-core') implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion - implementation 'com.google.guava:guava:' + guavaVersion + implementation ('com.google.guava:guava:' + guavaVersion) { + exclude group: 'com.google.code.findbugs', module: 'jsr305' + exclude group: 'org.checkerframework', module: 'checker-compat-qual' + exclude group: 'com.google.errorprone', module: 'error_prone_annotations' + exclude group: 'com.google.j2objc', module: 'j2objc-annotations' + exclude group: 'org.codehaus.mojo', module: 'animal-sniffer-annotations' + } compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion compileOnly 'org.jetbrains.kotlin:kotlin-annotations-jvm:' + kotlinAnnotationsVersion testImplementation project(modulePrefix + 'testutils') diff --git a/extensions/workmanager/build.gradle b/extensions/workmanager/build.gradle index f30461d379..8eab503d23 100644 --- a/extensions/workmanager/build.gradle +++ b/extensions/workmanager/build.gradle @@ -21,7 +21,13 @@ dependencies { // Guava & Gradle interact badly, and this prevents // "cannot access ListenableFuture" errors [internal b/157225611]. // More info: https://blog.gradle.org/guava - implementation 'com.google.guava:guava:' + guavaVersion + implementation ('com.google.guava:guava:' + guavaVersion) { + exclude group: 'com.google.code.findbugs', module: 'jsr305' + exclude group: 'org.checkerframework', module: 'checker-compat-qual' + exclude group: 'com.google.errorprone', module: 'error_prone_annotations' + exclude group: 'com.google.j2objc', module: 'j2objc-annotations' + exclude group: 'org.codehaus.mojo', module: 'animal-sniffer-annotations' + } compileOnly 'org.jetbrains.kotlin:kotlin-annotations-jvm:' + kotlinAnnotationsVersion } diff --git a/library/common/build.gradle b/library/common/build.gradle index d81201ccc1..2888b7e24c 100644 --- a/library/common/build.gradle +++ b/library/common/build.gradle @@ -17,7 +17,13 @@ android.buildTypes.debug.testCoverageEnabled true dependencies { implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion - implementation 'com.google.guava:guava:' + guavaVersion + implementation ('com.google.guava:guava:' + guavaVersion) { + exclude group: 'com.google.code.findbugs', module: 'jsr305' + exclude group: 'org.checkerframework', module: 'checker-compat-qual' + exclude group: 'com.google.errorprone', module: 'error_prone_annotations' + exclude group: 'com.google.j2objc', module: 'j2objc-annotations' + exclude group: 'org.codehaus.mojo', module: 'animal-sniffer-annotations' + } compileOnly 'com.google.code.findbugs:jsr305:' + jsr305Version compileOnly 'org.checkerframework:checker-compat-qual:' + checkerframeworkCompatVersion compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion diff --git a/library/core/build.gradle b/library/core/build.gradle index 6dc3dd647f..ddeb734947 100644 --- a/library/core/build.gradle +++ b/library/core/build.gradle @@ -37,19 +37,37 @@ dependencies { api project(modulePrefix + 'library-common') api project(modulePrefix + 'library-extractor') implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion - implementation 'com.google.guava:guava:' + guavaVersion + implementation ('com.google.guava:guava:' + guavaVersion) { + exclude group: 'com.google.code.findbugs', module: 'jsr305' + exclude group: 'org.checkerframework', module: 'checker-compat-qual' + exclude group: 'com.google.errorprone', module: 'error_prone_annotations' + exclude group: 'com.google.j2objc', module: 'j2objc-annotations' + exclude group: 'org.codehaus.mojo', module: 'animal-sniffer-annotations' + } compileOnly 'com.google.code.findbugs:jsr305:' + jsr305Version compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion compileOnly 'org.checkerframework:checker-compat-qual:' + checkerframeworkCompatVersion compileOnly 'org.jetbrains.kotlin:kotlin-annotations-jvm:' + kotlinAnnotationsVersion androidTestImplementation 'androidx.test:runner:' + androidxTestRunnerVersion - androidTestImplementation 'com.google.guava:guava:' + guavaVersion + androidTestImplementation ('com.google.guava:guava:' + guavaVersion) { + exclude group: 'com.google.code.findbugs', module: 'jsr305' + exclude group: 'org.checkerframework', module: 'checker-compat-qual' + exclude group: 'com.google.errorprone', module: 'error_prone_annotations' + exclude group: 'com.google.j2objc', module: 'j2objc-annotations' + exclude group: 'org.codehaus.mojo', module: 'animal-sniffer-annotations' + } androidTestImplementation 'com.linkedin.dexmaker:dexmaker:' + dexmakerVersion androidTestImplementation 'com.linkedin.dexmaker:dexmaker-mockito:' + dexmakerVersion androidTestImplementation(project(modulePrefix + 'testutils')) { exclude module: modulePrefix.substring(1) + 'library-core' } - testImplementation 'com.google.guava:guava:' + guavaVersion + testImplementation ('com.google.guava:guava:' + guavaVersion) { + exclude group: 'com.google.code.findbugs', module: 'jsr305' + exclude group: 'org.checkerframework', module: 'checker-compat-qual' + exclude group: 'com.google.errorprone', module: 'error_prone_annotations' + exclude group: 'com.google.j2objc', module: 'j2objc-annotations' + exclude group: 'org.codehaus.mojo', module: 'animal-sniffer-annotations' + } testImplementation 'com.squareup.okhttp3:mockwebserver:' + mockWebServerVersion testImplementation 'org.robolectric:robolectric:' + robolectricVersion testImplementation project(modulePrefix + 'testutils') diff --git a/library/dash/build.gradle b/library/dash/build.gradle index 82e17607f9..e6cb20d933 100644 --- a/library/dash/build.gradle +++ b/library/dash/build.gradle @@ -25,7 +25,13 @@ android { dependencies { implementation project(modulePrefix + 'library-core') - implementation 'com.google.guava:guava:' + guavaVersion + implementation ('com.google.guava:guava:' + guavaVersion) { + exclude group: 'com.google.code.findbugs', module: 'jsr305' + exclude group: 'org.checkerframework', module: 'checker-compat-qual' + exclude group: 'com.google.errorprone', module: 'error_prone_annotations' + exclude group: 'com.google.j2objc', module: 'j2objc-annotations' + exclude group: 'org.codehaus.mojo', module: 'animal-sniffer-annotations' + } compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion compileOnly 'org.checkerframework:checker-compat-qual:' + checkerframeworkCompatVersion compileOnly 'org.jetbrains.kotlin:kotlin-annotations-jvm:' + kotlinAnnotationsVersion diff --git a/library/extractor/build.gradle b/library/extractor/build.gradle index ffc1ce141e..82c2309c5f 100644 --- a/library/extractor/build.gradle +++ b/library/extractor/build.gradle @@ -26,7 +26,13 @@ android { dependencies { implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion implementation project(modulePrefix + 'library-common') - implementation 'com.google.guava:guava:' + guavaVersion + implementation ('com.google.guava:guava:' + guavaVersion) { + exclude group: 'com.google.code.findbugs', module: 'jsr305' + exclude group: 'org.checkerframework', module: 'checker-compat-qual' + exclude group: 'com.google.errorprone', module: 'error_prone_annotations' + exclude group: 'com.google.j2objc', module: 'j2objc-annotations' + exclude group: 'org.codehaus.mojo', module: 'animal-sniffer-annotations' + } compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion compileOnly 'org.checkerframework:checker-compat-qual:' + checkerframeworkCompatVersion compileOnly 'org.jetbrains.kotlin:kotlin-annotations-jvm:' + kotlinAnnotationsVersion diff --git a/library/hls/build.gradle b/library/hls/build.gradle index 80ef65117b..df3b6d3586 100644 --- a/library/hls/build.gradle +++ b/library/hls/build.gradle @@ -25,7 +25,13 @@ android { dependencies { implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion - implementation 'com.google.guava:guava:' + guavaVersion + implementation ('com.google.guava:guava:' + guavaVersion) { + exclude group: 'com.google.code.findbugs', module: 'jsr305' + exclude group: 'org.checkerframework', module: 'checker-compat-qual' + exclude group: 'com.google.errorprone', module: 'error_prone_annotations' + exclude group: 'com.google.j2objc', module: 'j2objc-annotations' + exclude group: 'org.codehaus.mojo', module: 'animal-sniffer-annotations' + } compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion compileOnly 'org.checkerframework:checker-compat-qual:' + checkerframeworkCompatVersion compileOnly 'org.jetbrains.kotlin:kotlin-annotations-jvm:' + kotlinAnnotationsVersion diff --git a/library/ui/build.gradle b/library/ui/build.gradle index 3825a15d92..f63e55b3b3 100644 --- a/library/ui/build.gradle +++ b/library/ui/build.gradle @@ -20,7 +20,13 @@ dependencies { api 'androidx.media:media:' + androidxMediaVersion implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion implementation 'androidx.recyclerview:recyclerview:' + androidxRecyclerViewVersion - implementation 'com.google.guava:guava:' + guavaVersion + implementation ('com.google.guava:guava:' + guavaVersion) { + exclude group: 'com.google.code.findbugs', module: 'jsr305' + exclude group: 'org.checkerframework', module: 'checker-compat-qual' + exclude group: 'com.google.errorprone', module: 'error_prone_annotations' + exclude group: 'com.google.j2objc', module: 'j2objc-annotations' + exclude group: 'org.codehaus.mojo', module: 'animal-sniffer-annotations' + } compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion compileOnly 'org.jetbrains.kotlin:kotlin-annotations-jvm:' + kotlinAnnotationsVersion testImplementation project(modulePrefix + 'testutils') From b6842cf0f08f42f6992f4eea42555ffd8447796e Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Fri, 11 Sep 2020 16:09:32 +0100 Subject: [PATCH 023/110] Fix handling of empty ad groups at non-integer cue points Issue: #7889 PiperOrigin-RevId: 331149688 --- RELEASENOTES.md | 2 + .../exoplayer2/ext/ima/ImaAdsLoader.java | 38 ++++++++++--------- .../exoplayer2/ext/ima/ImaAdsLoaderTest.java | 31 ++++++++++++--- 3 files changed, 48 insertions(+), 23 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index cd20e773d1..eebd6d42ff 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -317,6 +317,8 @@ * Add missing notification of `VideoAdPlayerCallback.onLoaded`. * Fix handling of incompatible VPAID ads ([#7832](https://github.com/google/ExoPlayer/issues/7832)). + * Fix handling of empty ads at non-integer cue points + ([#7889](https://github.com/google/ExoPlayer/issues/7889)). * Demo app: * Replace the `extensions` variant with `decoderExtensions` and update the demo app use the Cronet and IMA extensions by default. diff --git a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java index a5539248b6..88b0daac49 100644 --- a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java +++ b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java @@ -1072,12 +1072,11 @@ public final class ImaAdsLoader if (DEBUG) { Log.d(TAG, "Fetch error for ad at " + adGroupTimeSecondsString + " seconds"); } - int adGroupTimeSeconds = Integer.parseInt(adGroupTimeSecondsString); + double adGroupTimeSeconds = Double.parseDouble(adGroupTimeSecondsString); int adGroupIndex = - adGroupTimeSeconds == -1 + adGroupTimeSeconds == -1.0 ? adPlaybackState.adGroupCount - 1 - : Util.linearSearch( - adPlaybackState.adGroupTimesUs, C.MICROS_PER_SECOND * adGroupTimeSeconds); + : getAdGroupIndexForCuePointTimeSeconds(adGroupTimeSeconds); handleAdGroupFetchError(adGroupIndex); break; case CONTENT_PAUSE_REQUESTED: @@ -1514,20 +1513,8 @@ public final class ImaAdsLoader return adPlaybackState.adGroupCount - 1; } - // adPodInfo.podIndex may be 0-based or 1-based, so for now look up the cue point instead. We - // receive cue points from IMA SDK as floats. This code replicates the same calculation used to - // populate adGroupTimesUs (having truncated input back to float, to avoid failures if the - // behavior of the IMA SDK changes to provide greater precision in AdPodInfo). - long adPodTimeUs = - Math.round((double) ((float) adPodInfo.getTimeOffset()) * C.MICROS_PER_SECOND); - for (int adGroupIndex = 0; adGroupIndex < adPlaybackState.adGroupCount; adGroupIndex++) { - long adGroupTimeUs = adPlaybackState.adGroupTimesUs[adGroupIndex]; - if (adGroupTimeUs != C.TIME_END_OF_SOURCE - && Math.abs(adGroupTimeUs - adPodTimeUs) < THRESHOLD_AD_MATCH_US) { - return adGroupIndex; - } - } - throw new IllegalStateException("Failed to find cue point"); + // adPodInfo.podIndex may be 0-based or 1-based, so for now look up the cue point instead. + return getAdGroupIndexForCuePointTimeSeconds(adPodInfo.getTimeOffset()); } /** @@ -1547,6 +1534,21 @@ public final class ImaAdsLoader return adGroupIndex; } + private int getAdGroupIndexForCuePointTimeSeconds(double cuePointTimeSeconds) { + // We receive initial cue points from IMA SDK as floats. This code replicates the same + // calculation used to populate adGroupTimesUs (having truncated input back to float, to avoid + // failures if the behavior of the IMA SDK changes to provide greater precision). + long adPodTimeUs = Math.round((float) cuePointTimeSeconds * C.MICROS_PER_SECOND); + for (int adGroupIndex = 0; adGroupIndex < adPlaybackState.adGroupCount; adGroupIndex++) { + long adGroupTimeUs = adPlaybackState.adGroupTimesUs[adGroupIndex]; + if (adGroupTimeUs != C.TIME_END_OF_SOURCE + && Math.abs(adGroupTimeUs - adPodTimeUs) < THRESHOLD_AD_MATCH_US) { + return adGroupIndex; + } + } + throw new IllegalStateException("Failed to find cue point"); + } + private String getAdMediaInfoString(AdMediaInfo adMediaInfo) { @Nullable AdInfo adInfo = adInfoByAdMediaInfo.get(adMediaInfo); return "AdMediaInfo[" + adMediaInfo.getUrl() + (adInfo != null ? ", " + adInfo : "") + "]"; diff --git a/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoaderTest.java b/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoaderTest.java index ee0ea41e47..e32a199200 100644 --- a/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoaderTest.java +++ b/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoaderTest.java @@ -23,6 +23,7 @@ import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.inOrder; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -113,7 +114,6 @@ public final class ImaAdsLoaderTest { @Mock private ImaFactory mockImaFactory; @Mock private AdPodInfo mockAdPodInfo; @Mock private Ad mockPrerollSingleAd; - @Mock private AdEvent mockPostrollFetchErrorAdEvent; private ViewGroup adViewGroup; private AdsLoader.AdViewProvider adViewProvider; @@ -290,8 +290,33 @@ public final class ImaAdsLoaderTest { .withAdResumePositionUs(/* adResumePositionUs= */ 0)); } + @Test + public void playback_withMidrollFetchError_marksAdAsInErrorState() { + AdEvent mockMidrollFetchErrorAdEvent = mock(AdEvent.class); + when(mockMidrollFetchErrorAdEvent.getType()).thenReturn(AdEventType.AD_BREAK_FETCH_ERROR); + when(mockMidrollFetchErrorAdEvent.getAdData()) + .thenReturn(ImmutableMap.of("adBreakTime", "20.5")); + setupPlayback(CONTENT_TIMELINE, ImmutableList.of(20.5f)); + + // Simulate loading an empty midroll ad. + imaAdsLoader.start(adsLoaderListener, adViewProvider); + adEventListener.onAdEvent(mockMidrollFetchErrorAdEvent); + + assertThat(adsLoaderListener.adPlaybackState) + .isEqualTo( + new AdPlaybackState(/* adGroupTimesUs...= */ 20_500_000) + .withContentDurationUs(CONTENT_PERIOD_DURATION_US) + .withAdDurationsUs(new long[][] {{TEST_AD_DURATION_US}}) + .withAdCount(/* adGroupIndex= */ 0, /* adCount= */ 1) + .withAdLoadError(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0)); + } + @Test public void playback_withPostrollFetchError_marksAdAsInErrorState() { + AdEvent mockPostrollFetchErrorAdEvent = mock(AdEvent.class); + when(mockPostrollFetchErrorAdEvent.getType()).thenReturn(AdEventType.AD_BREAK_FETCH_ERROR); + when(mockPostrollFetchErrorAdEvent.getAdData()) + .thenReturn(ImmutableMap.of("adBreakTime", "-1")); setupPlayback(CONTENT_TIMELINE, ImmutableList.of(-1f)); // Simulate loading an empty postroll ad. @@ -808,10 +833,6 @@ public final class ImaAdsLoaderTest { when(mockAdPodInfo.getAdPosition()).thenReturn(1); when(mockPrerollSingleAd.getAdPodInfo()).thenReturn(mockAdPodInfo); - - when(mockPostrollFetchErrorAdEvent.getType()).thenReturn(AdEventType.AD_BREAK_FETCH_ERROR); - when(mockPostrollFetchErrorAdEvent.getAdData()) - .thenReturn(ImmutableMap.of("adBreakTime", "-1")); } private static AdEvent getAdEvent(AdEventType adEventType, @Nullable Ad ad) { From a8298fa7469a54c789089e483cbdacdd5e54e6d6 Mon Sep 17 00:00:00 2001 From: bachinger Date: Fri, 11 Sep 2020 16:48:09 +0100 Subject: [PATCH 024/110] MediaItemify the IMA extension README and the ads page in dev guide PiperOrigin-RevId: 331155539 --- extensions/ima/README.md | 31 ++++++++++++++++++------------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/extensions/ima/README.md b/extensions/ima/README.md index 0a9bf1aa5e..54a70bd5fb 100644 --- a/extensions/ima/README.md +++ b/extensions/ima/README.md @@ -26,25 +26,30 @@ locally. Instructions for doing this can be found in ExoPlayer's ## Using the extension ## -To play ads alongside a single-window content `MediaSource`, prepare the player -with an `AdsMediaSource` constructed using an `ImaAdsLoader`, the content -`MediaSource` and an overlay `ViewGroup` on top of the player. Pass an ad tag -URI from your ad campaign when creating the `ImaAdsLoader`. The IMA -documentation includes some [sample ad tags][] for testing. Note that the IMA -extension only supports players which are accessed on the application's main +To play a media item with an ad tag URI, you need to customize the +`DefaultMediaSourceFactory` as +[documented in the Developer Guide](https://exoplayer.dev/media-sources.html#customizing-media-source-creation). +This way the player will build an `AdsMediaSource` and configure it with the +`ImaAdsLoader` and an `AdViewProvider` automatically. + +[Pass an ad tag URI](https://exoplayer.dev/media-items.html#ad-insertion) from +your ad campaign to the `MediaItem.Builder` when building your media item. The +IMA documentation includes some [sample ad tags][] for testing. Note that the +IMA extension only supports players which are accessed on the application's main thread. Resuming the player after entering the background requires some special handling when playing ads. The player and its media source are released on entering the background, and are recreated when the player returns to the foreground. When playing ads it is necessary to persist ad playback state while in the background -by keeping a reference to the `ImaAdsLoader`. Reuse it when resuming playback of -the same content/ads by passing it in when constructing the new -`AdsMediaSource`. It is also important to persist the player position when -entering the background by storing the value of `player.getContentPosition()`. -On returning to the foreground, seek to that position before preparing the new -player instance. Finally, it is important to call `ImaAdsLoader.release()` when -playback of the content/ads has finished and will not be resumed. +by keeping a reference to the `ImaAdsLoader`. Reuse this instance when your +callback `AdsLoaderProvider.getAdsLoader(Uri adTagUri)` is called by the player +to get an `ImaAdsLoader` for the same content/ads to be resumed. It is also +important to persist the player position when entering the background by storing +the value of `player.getContentPosition()`. On returning to the foreground, seek +to that position before preparing the new player instance. Finally, it is +important to call `ImaAdsLoader.release()` when playback of the content/ads has +finished and will not be resumed. You can try the IMA extension in the ExoPlayer demo app, which has test content in the "IMA sample ad tags" section of the sample chooser. The demo app's From 12b0537c2f3afabd9b53c234cf922cdebc62a2ed Mon Sep 17 00:00:00 2001 From: olly Date: Fri, 11 Sep 2020 17:27:56 +0100 Subject: [PATCH 025/110] Clean up some lint warnings PiperOrigin-RevId: 331162350 --- demos/cast/build.gradle | 2 +- demos/main/build.gradle | 2 +- .../exoplayer2/demo/DownloadTracker.java | 90 ++++++++++--------- demos/main/src/main/res/values/strings.xml | 6 -- extensions/media2/build.gradle | 2 +- extensions/workmanager/build.gradle | 2 +- 6 files changed, 53 insertions(+), 51 deletions(-) diff --git a/demos/cast/build.gradle b/demos/cast/build.gradle index b26112e15a..868e3c7b43 100644 --- a/demos/cast/build.gradle +++ b/demos/cast/build.gradle @@ -60,7 +60,7 @@ dependencies { implementation 'androidx.appcompat:appcompat:' + androidxAppCompatVersion implementation 'androidx.multidex:multidex:' + androidxMultidexVersion implementation 'androidx.recyclerview:recyclerview:1.1.0' - implementation 'com.google.android.material:material:1.1.0' + implementation 'com.google.android.material:material:1.2.1' } apply plugin: 'com.google.android.gms.strict-version-matcher-plugin' diff --git a/demos/main/build.gradle b/demos/main/build.gradle index 3a3b7a4a45..716b3c1f99 100644 --- a/demos/main/build.gradle +++ b/demos/main/build.gradle @@ -69,7 +69,7 @@ dependencies { implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion implementation 'androidx.appcompat:appcompat:' + androidxAppCompatVersion implementation 'androidx.multidex:multidex:' + androidxMultidexVersion - implementation 'com.google.android.material:material:1.2.0' + implementation 'com.google.android.material:material:1.2.1' implementation ('com.google.guava:guava:' + guavaVersion) { exclude group: 'com.google.code.findbugs', module: 'jsr305' exclude group: 'org.checkerframework', module: 'checker-compat-qual' diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/DownloadTracker.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/DownloadTracker.java index b448dd40de..07f4dd2f6e 100644 --- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/DownloadTracker.java +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/DownloadTracker.java @@ -222,7 +222,11 @@ public class DownloadTracker { } widevineOfflineLicenseFetchTask = new WidevineOfflineLicenseFetchTask( - format, mediaItem.playbackProperties.drmConfiguration.licenseUri, this, helper); + format, + mediaItem.playbackProperties.drmConfiguration.licenseUri, + httpDataSourceFactory, + /* dialogHelper= */ this, + helper); widevineOfflineLicenseFetchTask.execute(); } @@ -271,6 +275,32 @@ public class DownloadTracker { // Internal methods. + /** + * Returns the first {@link Format} with a non-null {@link Format#drmInitData} found in the + * content's tracks, or null if none is found. + */ + @Nullable + private Format getFirstFormatWithDrmInitData(DownloadHelper helper) { + for (int periodIndex = 0; periodIndex < helper.getPeriodCount(); periodIndex++) { + MappedTrackInfo mappedTrackInfo = helper.getMappedTrackInfo(periodIndex); + for (int rendererIndex = 0; + rendererIndex < mappedTrackInfo.getRendererCount(); + rendererIndex++) { + TrackGroupArray trackGroups = mappedTrackInfo.getTrackGroups(rendererIndex); + for (int trackGroupIndex = 0; trackGroupIndex < trackGroups.length; trackGroupIndex++) { + TrackGroup trackGroup = trackGroups.get(trackGroupIndex); + for (int formatIndex = 0; formatIndex < trackGroup.length; formatIndex++) { + Format format = trackGroup.getFormat(formatIndex); + if (format.drmInitData != null) { + return format; + } + } + } + } + } + return null; + } + private void onOfflineLicenseFetched(DownloadHelper helper, byte[] keySetId) { this.keySetId = keySetId; onDownloadPrepared(helper); @@ -309,6 +339,19 @@ public class DownloadTracker { trackSelectionDialog.show(fragmentManager, /* tag= */ null); } + /** + * Returns whether any the {@link DrmInitData.SchemeData} contained in {@code drmInitData} has + * non-null {@link DrmInitData.SchemeData#data}. + */ + private boolean hasSchemaData(DrmInitData drmInitData) { + for (int i = 0; i < drmInitData.schemeDataCount; i++) { + if (drmInitData.get(i).hasData()) { + return true; + } + } + return false; + } + private void startDownload() { startDownload(buildDownloadRequest()); } @@ -327,9 +370,11 @@ public class DownloadTracker { /** Downloads a Widevine offline license in a background thread. */ @RequiresApi(18) - private final class WidevineOfflineLicenseFetchTask extends AsyncTask { + private static final class WidevineOfflineLicenseFetchTask extends AsyncTask { + private final Format format; private final Uri licenseUri; + private final HttpDataSource.Factory httpDataSourceFactory; private final StartDownloadDialogHelper dialogHelper; private final DownloadHelper downloadHelper; @@ -339,10 +384,12 @@ public class DownloadTracker { public WidevineOfflineLicenseFetchTask( Format format, Uri licenseUri, + HttpDataSource.Factory httpDataSourceFactory, StartDownloadDialogHelper dialogHelper, DownloadHelper downloadHelper) { this.format = format; this.licenseUri = licenseUri; + this.httpDataSourceFactory = httpDataSourceFactory; this.dialogHelper = dialogHelper; this.downloadHelper = downloadHelper; } @@ -373,43 +420,4 @@ public class DownloadTracker { } } } - - /** - * Returns whether any the {@link DrmInitData.SchemeData} contained in {@code drmInitData} has - * non-null {@link DrmInitData.SchemeData#data}. - */ - private static boolean hasSchemaData(DrmInitData drmInitData) { - for (int i = 0; i < drmInitData.schemeDataCount; i++) { - if (drmInitData.get(i).hasData()) { - return true; - } - } - return false; - } - - /** - * Returns the first {@link Format} with a non-null {@link Format#drmInitData} found in the - * content's tracks, or null if none is found. - */ - @Nullable - private Format getFirstFormatWithDrmInitData(DownloadHelper helper) { - for (int periodIndex = 0; periodIndex < helper.getPeriodCount(); periodIndex++) { - MappedTrackInfo mappedTrackInfo = helper.getMappedTrackInfo(periodIndex); - for (int rendererIndex = 0; - rendererIndex < mappedTrackInfo.getRendererCount(); - rendererIndex++) { - TrackGroupArray trackGroups = mappedTrackInfo.getTrackGroups(rendererIndex); - for (int trackGroupIndex = 0; trackGroupIndex < trackGroups.length; trackGroupIndex++) { - TrackGroup trackGroup = trackGroups.get(trackGroupIndex); - for (int formatIndex = 0; formatIndex < trackGroup.length; formatIndex++) { - Format format = trackGroup.getFormat(formatIndex); - if (format.drmInitData != null) { - return format; - } - } - } - } - } - return null; - } } diff --git a/demos/main/src/main/res/values/strings.xml b/demos/main/src/main/res/values/strings.xml index 3f80b70184..bd5cd63467 100644 --- a/demos/main/src/main/res/values/strings.xml +++ b/demos/main/src/main/res/values/strings.xml @@ -25,16 +25,10 @@ Playback failed - Unrecognized ABR algorithm - - Unrecognized stereo mode - DRM content not supported on API levels below 18 This device does not support the required DRM scheme - An unknown DRM error occurred - This device does not provide a decoder for %1$s This device does not provide a secure decoder for %1$s diff --git a/extensions/media2/build.gradle b/extensions/media2/build.gradle index cedb4cd55f..744d79980b 100644 --- a/extensions/media2/build.gradle +++ b/extensions/media2/build.gradle @@ -18,7 +18,7 @@ android.defaultConfig.minSdkVersion 19 dependencies { implementation project(modulePrefix + 'library-core') implementation 'androidx.collection:collection:' + androidxCollectionVersion - implementation 'androidx.concurrent:concurrent-futures:1.0.0' + implementation 'androidx.concurrent:concurrent-futures:1.1.0' implementation ('com.google.guava:guava:' + guavaVersion) { exclude group: 'com.google.code.findbugs', module: 'jsr305' exclude group: 'org.checkerframework', module: 'checker-compat-qual' diff --git a/extensions/workmanager/build.gradle b/extensions/workmanager/build.gradle index 8eab503d23..1882ebac81 100644 --- a/extensions/workmanager/build.gradle +++ b/extensions/workmanager/build.gradle @@ -17,7 +17,7 @@ apply from: "$gradle.ext.exoplayerSettingsDir/common_library_config.gradle" dependencies { implementation project(modulePrefix + 'library-core') - implementation 'androidx.work:work-runtime:2.3.4' + implementation 'androidx.work:work-runtime:2.4.0' // Guava & Gradle interact badly, and this prevents // "cannot access ListenableFuture" errors [internal b/157225611]. // More info: https://blog.gradle.org/guava From 4f982763cf68af3ee7a4ad9f04e5391da47eaa65 Mon Sep 17 00:00:00 2001 From: bachinger Date: Fri, 11 Sep 2020 21:27:43 +0100 Subject: [PATCH 026/110] Add getMediaItemCount() and getMediaItemAt(int) PiperOrigin-RevId: 331211708 --- RELEASENOTES.md | 7 ++++--- .../java/com/google/android/exoplayer2/BasePlayer.java | 10 ++++++++++ .../java/com/google/android/exoplayer2/Player.java | 6 ++++++ 3 files changed, 20 insertions(+), 3 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index eebd6d42ff..a4ae7bb05a 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -1,6 +1,6 @@ # Release notes -### 2.12.0 (2020-09-03) ### +### 2.12.0 (2020-09-11) ### * Core library: * `Player`: @@ -8,8 +8,9 @@ ([#6161](https://github.com/google/ExoPlayer/issues/6161)). The new methods for playlist manipulation are `setMediaItem(s)`, `addMediaItem(s)`, `moveMediaItem(s)`, `removeMediaItem(s)` and - `clearMediaItems`. This API should be used instead of - `ConcatenatingMediaSource` in most cases. + `clearMediaItems`. The playlist can be queried using + `getMediaItemCount` and `getMediaItemAt`. This API should be used + instead of `ConcatenatingMediaSource` in most cases. * Add `getCurrentMediaItem` for getting the currently playing item in the playlist. * Add `EventListener.onMediaItemTransition` to report when diff --git a/library/core/src/main/java/com/google/android/exoplayer2/BasePlayer.java b/library/core/src/main/java/com/google/android/exoplayer2/BasePlayer.java index 893c512bd7..9d7af2dce6 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/BasePlayer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/BasePlayer.java @@ -185,6 +185,16 @@ public abstract class BasePlayer implements Player { : timeline.getWindow(getCurrentWindowIndex(), window).mediaItem; } + @Override + public int getMediaItemCount() { + return getCurrentTimeline().getWindowCount(); + } + + @Override + public MediaItem getMediaItemAt(int index) { + return getCurrentTimeline().getWindow(index, window).mediaItem; + } + @Override @Nullable public final Object getCurrentManifest() { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/Player.java b/library/core/src/main/java/com/google/android/exoplayer2/Player.java index 9d9d9cdc2d..7a52aae738 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/Player.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/Player.java @@ -1284,6 +1284,12 @@ public interface Player { @Nullable MediaItem getCurrentMediaItem(); + /** Returns the number of {@link MediaItem media items} in the playlist. */ + int getMediaItemCount(); + + /** Returns the {@link MediaItem} at the given index. */ + MediaItem getMediaItemAt(int index); + /** * Returns the duration of the current content window or ad in milliseconds, or {@link * C#TIME_UNSET} if the duration is not known. From a5bf3f41913c5f41d79df29df2ba6982071ac774 Mon Sep 17 00:00:00 2001 From: olly Date: Sat, 12 Sep 2020 19:08:22 +0100 Subject: [PATCH 027/110] Remove references to cross-protocol redirects for Cronet There's no option to enable them. This is probably a copy/paste error from DefaultHttpDataSourceFactory. PiperOrigin-RevId: 331334263 --- .../ext/cronet/CronetDataSourceFactory.java | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetDataSourceFactory.java b/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetDataSourceFactory.java index 4590936ea5..85c9d09a79 100644 --- a/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetDataSourceFactory.java +++ b/extensions/cronet/src/main/java/com/google/android/exoplayer2/ext/cronet/CronetDataSourceFactory.java @@ -58,8 +58,7 @@ public final class CronetDataSourceFactory extends BaseFactory { * fallback {@link HttpDataSource.Factory} will be used instead. * *

    Sets {@link CronetDataSource#DEFAULT_CONNECT_TIMEOUT_MILLIS} as the connection timeout, - * {@link CronetDataSource#DEFAULT_READ_TIMEOUT_MILLIS} as the read timeout and disables - * cross-protocol redirects. + * {@link CronetDataSource#DEFAULT_READ_TIMEOUT_MILLIS} as the read timeout. * * @param cronetEngineWrapper A {@link CronetEngineWrapper}. * @param executor The {@link java.util.concurrent.Executor} that will perform the requests. @@ -87,8 +86,7 @@ public final class CronetDataSourceFactory extends BaseFactory { * DefaultHttpDataSourceFactory} will be used instead. * *

    Sets {@link CronetDataSource#DEFAULT_CONNECT_TIMEOUT_MILLIS} as the connection timeout, - * {@link CronetDataSource#DEFAULT_READ_TIMEOUT_MILLIS} as the read timeout and disables - * cross-protocol redirects. + * {@link CronetDataSource#DEFAULT_READ_TIMEOUT_MILLIS} as the read timeout. * * @param cronetEngineWrapper A {@link CronetEngineWrapper}. * @param executor The {@link java.util.concurrent.Executor} that will perform the requests. @@ -104,8 +102,7 @@ public final class CronetDataSourceFactory extends BaseFactory { * DefaultHttpDataSourceFactory} will be used instead. * *

    Sets {@link CronetDataSource#DEFAULT_CONNECT_TIMEOUT_MILLIS} as the connection timeout, - * {@link CronetDataSource#DEFAULT_READ_TIMEOUT_MILLIS} as the read timeout and disables - * cross-protocol redirects. + * {@link CronetDataSource#DEFAULT_READ_TIMEOUT_MILLIS} as the read timeout. * * @param cronetEngineWrapper A {@link CronetEngineWrapper}. * @param executor The {@link java.util.concurrent.Executor} that will perform the requests. @@ -201,8 +198,7 @@ public final class CronetDataSourceFactory extends BaseFactory { * fallback {@link HttpDataSource.Factory} will be used instead. * *

    Sets {@link CronetDataSource#DEFAULT_CONNECT_TIMEOUT_MILLIS} as the connection timeout, - * {@link CronetDataSource#DEFAULT_READ_TIMEOUT_MILLIS} as the read timeout and disables - * cross-protocol redirects. + * {@link CronetDataSource#DEFAULT_READ_TIMEOUT_MILLIS} as the read timeout. * * @param cronetEngineWrapper A {@link CronetEngineWrapper}. * @param executor The {@link java.util.concurrent.Executor} that will perform the requests. @@ -232,8 +228,7 @@ public final class CronetDataSourceFactory extends BaseFactory { * DefaultHttpDataSourceFactory} will be used instead. * *

    Sets {@link CronetDataSource#DEFAULT_CONNECT_TIMEOUT_MILLIS} as the connection timeout, - * {@link CronetDataSource#DEFAULT_READ_TIMEOUT_MILLIS} as the read timeout and disables - * cross-protocol redirects. + * {@link CronetDataSource#DEFAULT_READ_TIMEOUT_MILLIS} as the read timeout. * * @param cronetEngineWrapper A {@link CronetEngineWrapper}. * @param executor The {@link java.util.concurrent.Executor} that will perform the requests. @@ -253,8 +248,7 @@ public final class CronetDataSourceFactory extends BaseFactory { * DefaultHttpDataSourceFactory} will be used instead. * *

    Sets {@link CronetDataSource#DEFAULT_CONNECT_TIMEOUT_MILLIS} as the connection timeout, - * {@link CronetDataSource#DEFAULT_READ_TIMEOUT_MILLIS} as the read timeout and disables - * cross-protocol redirects. + * {@link CronetDataSource#DEFAULT_READ_TIMEOUT_MILLIS} as the read timeout. * * @param cronetEngineWrapper A {@link CronetEngineWrapper}. * @param executor The {@link java.util.concurrent.Executor} that will perform the requests. From 3f6e5ce676e07ff457c1bc82b280cd1c3f769c4d Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Sat, 12 Sep 2020 21:28:58 +0100 Subject: [PATCH 028/110] Update IMA readme --- extensions/ima/README.md | 39 +++++++++++++++------------------------ 1 file changed, 15 insertions(+), 24 deletions(-) diff --git a/extensions/ima/README.md b/extensions/ima/README.md index 54a70bd5fb..4da7a8f22e 100644 --- a/extensions/ima/README.md +++ b/extensions/ima/README.md @@ -26,39 +26,30 @@ locally. Instructions for doing this can be found in ExoPlayer's ## Using the extension ## -To play a media item with an ad tag URI, you need to customize the -`DefaultMediaSourceFactory` as -[documented in the Developer Guide](https://exoplayer.dev/media-sources.html#customizing-media-source-creation). -This way the player will build an `AdsMediaSource` and configure it with the -`ImaAdsLoader` and an `AdViewProvider` automatically. - -[Pass an ad tag URI](https://exoplayer.dev/media-items.html#ad-insertion) from -your ad campaign to the `MediaItem.Builder` when building your media item. The -IMA documentation includes some [sample ad tags][] for testing. Note that the -IMA extension only supports players which are accessed on the application's main -thread. +To use the extension, follow the instructions on the +[Ad insertion page](https://exoplayer.dev//ad-insertion.html#declarative-ad-support) +of the developer guide, passing an `AdsLoaderProvider` that returns an +`ImaAdsLoader` when configuring the player's `DefaultMediaSourceFactory`. Note +that the IMA extension only supports players which are accessed on the +application's main thread. Resuming the player after entering the background requires some special handling when playing ads. The player and its media source are released on entering the -background, and are recreated when the player returns to the foreground. When -playing ads it is necessary to persist ad playback state while in the background -by keeping a reference to the `ImaAdsLoader`. Reuse this instance when your -callback `AdsLoaderProvider.getAdsLoader(Uri adTagUri)` is called by the player -to get an `ImaAdsLoader` for the same content/ads to be resumed. It is also -important to persist the player position when entering the background by storing -the value of `player.getContentPosition()`. On returning to the foreground, seek -to that position before preparing the new player instance. Finally, it is -important to call `ImaAdsLoader.release()` when playback of the content/ads has -finished and will not be resumed. +background, and are recreated when returning to the foreground. When playing ads +it is necessary to persist ad playback state while in the background by keeping +a reference to the `ImaAdsLoader`. When re-entering the foreground, pass the +same instance back when `AdsLoaderProvider.getAdsLoader(Uri adTagUri)` is called +to restore the state. It is also important to persist the player position when +entering the background by storing the value of `player.getContentPosition()`. +On returning to the foreground, seek to that position before preparing the new +player instance. Finally, it is important to call `ImaAdsLoader.release()` when +playback has finished and will not be resumed. You can try the IMA extension in the ExoPlayer demo app, which has test content in the "IMA sample ad tags" section of the sample chooser. The demo app's `PlayerActivity` also shows how to persist the `ImaAdsLoader` instance and the player position when backgrounded during ad playback. -[top level README]: https://github.com/google/ExoPlayer/blob/release-v2/README.md -[sample ad tags]: https://developers.google.com/interactive-media-ads/docs/sdks/android/tags - ## Links ## * [ExoPlayer documentation on ad insertion][] From 4ab966dc96eeb195a1ae9a11a048106d07e8857a Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Sat, 12 Sep 2020 22:19:02 +0100 Subject: [PATCH 029/110] Improve IMA readme --- extensions/ima/README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/extensions/ima/README.md b/extensions/ima/README.md index 4da7a8f22e..c67dfdbb5d 100644 --- a/extensions/ima/README.md +++ b/extensions/ima/README.md @@ -27,11 +27,11 @@ locally. Instructions for doing this can be found in ExoPlayer's ## Using the extension ## To use the extension, follow the instructions on the -[Ad insertion page](https://exoplayer.dev//ad-insertion.html#declarative-ad-support) -of the developer guide, passing an `AdsLoaderProvider` that returns an -`ImaAdsLoader` when configuring the player's `DefaultMediaSourceFactory`. Note -that the IMA extension only supports players which are accessed on the -application's main thread. +[Ad insertion page](https://exoplayer.dev/ad-insertion.html#declarative-ad-support) +of the developer guide. The `AdsLoaderProvider` passed to the player's +`DefaultMediaSourceFactory` should return an `ImaAdsLoader`. Note that the IMA +extension only supports players which are accessed on the application's main +thread. Resuming the player after entering the background requires some special handling when playing ads. The player and its media source are released on entering the From d2fa7b0217d069cda4d2588ad39e17f042093a28 Mon Sep 17 00:00:00 2001 From: olly Date: Sun, 13 Sep 2020 00:01:14 +0100 Subject: [PATCH 030/110] Fix release notes PiperOrigin-RevId: 331354102 --- RELEASENOTES.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index a4ae7bb05a..951c93cf8b 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -150,8 +150,7 @@ * Add support for default [text](https://www.w3.org/TR/webvtt1/#default-text-color) and [background](https://www.w3.org/TR/webvtt1/#default-text-background) - colors ([PR #4178](https://github.com/google/ExoPlayer/pull/4178), - [issue #6581](https://github.com/google/ExoPlayer/issues/6581)). + colors ([#6581](https://github.com/google/ExoPlayer/issues/6581)). * Update position alignment parsing to recognise `line-left`, `center` and `line-right`. * Implement steps 4-10 of the @@ -209,7 +208,7 @@ ([#7308](https://github.com/google/ExoPlayer/issues/7308)). * Matroska: * Support Dolby Vision - ([#7267](https://github.com/google/ExoPlayer/issues/7267). + ([#7267](https://github.com/google/ExoPlayer/issues/7267)). * Populate `Format.label` with track titles. * Remove support for the `Invisible` block header flag. * MPEG-TS: Add support for MPEG-4 Part 2 and H.263 From 9a01e6cf1e67da274f15d82168fb4978fd15b387 Mon Sep 17 00:00:00 2001 From: christosts Date: Mon, 14 Sep 2020 15:03:55 +0100 Subject: [PATCH 031/110] Add release note entry for DRM-protected downloads PiperOrigin-RevId: 331539036 --- RELEASENOTES.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 951c93cf8b..739fa943f2 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -334,6 +334,8 @@ * Remove support for media tunneling, random ABR and playback of spherical video. Developers wishing to experiment with these features can enable them by modifying the demo app source code. + * Add support for downloading DRM-protected content using offline + Widevine licenses. ### 2.11.8 (2020-08-25) ### From 8386d617bfeeed310b48996b095ec8fb8901cb65 Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 15 Sep 2020 13:34:49 +0100 Subject: [PATCH 032/110] Fix OOM-is-prevented test OOM-ing :) This test is intended to check that DefaultLoadControl will cause playback to fail as "stuck buffering" rather than OOM-ing, in the case that its target buffer size is reached and playback still hasn't started. Unfortunately, the target buffer size is ~130MB, and when running on some setups an OOM actually ends up happening before this much memory is allocated. This change makes the target buffer size much smaller to avoid the problem. PiperOrigin-RevId: 331748208 --- .../java/com/google/android/exoplayer2/ExoPlayerTest.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/library/core/src/test/java/com/google/android/exoplayer2/ExoPlayerTest.java b/library/core/src/test/java/com/google/android/exoplayer2/ExoPlayerTest.java index b8f10a2dc2..444640256f 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/ExoPlayerTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/ExoPlayerTest.java @@ -7330,6 +7330,10 @@ public final class ExoPlayerTest { @Test public void infiniteLoading_withSmallAllocations_oomIsPreventedByLoadControl_andThrowsStuckBufferingIllegalStateException() { + DefaultLoadControl loadControl = + new DefaultLoadControl.Builder() + .setTargetBufferBytes(10 * C.DEFAULT_BUFFER_SEGMENT_SIZE) + .build(); MediaSource continuouslyAllocatingMediaSource = new FakeMediaSource( new FakeTimeline(/* windowCount= */ 1), ExoPlayerTestRunner.VIDEO_FORMAT) { @@ -7387,6 +7391,7 @@ public final class ExoPlayerTest { new ExoPlayerTestRunner.Builder(context) .setActionSchedule(actionSchedule) .setMediaSources(continuouslyAllocatingMediaSource) + .setLoadControl(loadControl) .build(); ExoPlaybackException exception = From 3fd47b92e625d66dab3f4cf18421e3540fcafb6c Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 15 Sep 2020 16:12:39 +0100 Subject: [PATCH 033/110] Add missing release note Issue: #7902 PiperOrigin-RevId: 331771187 --- RELEASENOTES.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 739fa943f2..966c27ece0 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -254,6 +254,9 @@ to `DownloadManager.Listener.onDownloadChanged`. * Support multiple non-overlapping write locks for the same key in `SimpleCache`. + * Remove `CacheUtil`. Equivalent functionality is provided by a new + `CacheWriter` class, `Cache.getCachedBytes`, `Cache.removeResource` and + `CacheKeyFactory.DEFAULT`. * DRM: * Remove previously deprecated APIs to inject `DrmSessionManager` into `Renderer` instances. `DrmSessionManager` must now be injected into From bc89a8f7638cdee2da99854ca80c815f02b8d5c7 Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 15 Sep 2020 16:39:57 +0100 Subject: [PATCH 034/110] Depend on robolectric 4.4, which has now been released Issue: #7906 PiperOrigin-RevId: 331775990 --- constants.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/constants.gradle b/constants.gradle index f613676953..c2b0000368 100644 --- a/constants.gradle +++ b/constants.gradle @@ -24,7 +24,7 @@ project.ext { guavaVersion = '27.1-android' mockitoVersion = '2.28.2' mockWebServerVersion = '3.12.0' - robolectricVersion = '4.4-SNAPSHOT' + robolectricVersion = '4.4' checkerframeworkVersion = '3.3.0' checkerframeworkCompatVersion = '2.5.0' jsr305Version = '3.0.2' From 52826c860fbc37f5a53bde3c90e312e59b9b6f01 Mon Sep 17 00:00:00 2001 From: christosts Date: Wed, 16 Sep 2020 09:35:07 +0100 Subject: [PATCH 035/110] Fix bug in offline DRM downloads PiperOrigin-RevId: 331955966 --- .../android/exoplayer2/demo/PlayerActivity.java | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java index 49fe440101..eae302887e 100644 --- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java @@ -543,7 +543,19 @@ public class PlayerActivity extends AppCompatActivity @Nullable DownloadRequest downloadRequest = downloadTracker.getDownloadRequest(checkNotNull(item.playbackProperties).uri); - mediaItems.add(downloadRequest != null ? downloadRequest.toMediaItem() : item); + if (downloadRequest != null) { + MediaItem.Builder builder = item.buildUpon(); + builder + .setMediaId(downloadRequest.id) + .setUri(downloadRequest.uri) + .setCustomCacheKey(downloadRequest.customCacheKey) + .setMimeType(downloadRequest.mimeType) + .setStreamKeys(downloadRequest.streamKeys) + .setDrmKeySetId(downloadRequest.keySetId); + mediaItems.add(builder.build()); + } else { + mediaItems.add(item); + } } return mediaItems; } From fe2bc7ba717b55752e7f7225d6dc30ebf7761d24 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Wed, 16 Sep 2020 16:33:09 +0100 Subject: [PATCH 036/110] Fix the FFmpeg extension build Since gradle 4.0, CMake imported libraries are bundled in the APK, so manually bundling them causes a duplication which breaks the build. Issue: #7906 PiperOrigin-RevId: 332012375 --- extensions/ffmpeg/build.gradle | 7 ------- extensions/ffmpeg/src/main/jni/CMakeLists.txt | 2 -- 2 files changed, 9 deletions(-) diff --git a/extensions/ffmpeg/build.gradle b/extensions/ffmpeg/build.gradle index bf715b7760..a9edeaff6b 100644 --- a/extensions/ffmpeg/build.gradle +++ b/extensions/ffmpeg/build.gradle @@ -13,13 +13,6 @@ // limitations under the License. apply from: "$gradle.ext.exoplayerSettingsDir/common_library_config.gradle" -android { - sourceSets.main { - // The directory from which to pick the ffmpeg binaries. - jniLibs.srcDir 'src/main/libs' - } -} - // Configure the native build only if ffmpeg is present to avoid gradle sync // failures if ffmpeg hasn't been built according to the README instructions. if (project.file('src/main/jni/ffmpeg').exists()) { diff --git a/extensions/ffmpeg/src/main/jni/CMakeLists.txt b/extensions/ffmpeg/src/main/jni/CMakeLists.txt index 4f16509721..b60af4fa18 100644 --- a/extensions/ffmpeg/src/main/jni/CMakeLists.txt +++ b/extensions/ffmpeg/src/main/jni/CMakeLists.txt @@ -7,7 +7,6 @@ project(libffmpeg_jni C CXX) set(ffmpeg_location "${CMAKE_CURRENT_SOURCE_DIR}/ffmpeg") set(ffmpeg_binaries "${ffmpeg_location}/android-libs/${ANDROID_ABI}") -set(ffmpeg_output_dir "${CMAKE_CURRENT_SOURCE_DIR}/../libs/${ANDROID_ABI}") foreach(ffmpeg_lib avutil swresample avcodec) set(ffmpeg_lib_filename lib${ffmpeg_lib}.so) @@ -20,7 +19,6 @@ foreach(ffmpeg_lib avutil swresample avcodec) ${ffmpeg_lib} PROPERTIES IMPORTED_LOCATION ${ffmpeg_lib_file_path}) - file(COPY ${ffmpeg_lib_file_path} DESTINATION ${ffmpeg_output_dir}) endforeach() include_directories(${ffmpeg_location}) From d1ce5f5b24127d12bd6d9fb0a23659ef377f9412 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Wed, 16 Sep 2020 16:35:26 +0100 Subject: [PATCH 037/110] Update the FFmpeg extension readme to use symlinking PiperOrigin-RevId: 332012857 --- extensions/ffmpeg/README.md | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/extensions/ffmpeg/README.md b/extensions/ffmpeg/README.md index ed5d2aba32..f3001427b8 100644 --- a/extensions/ffmpeg/README.md +++ b/extensions/ffmpeg/README.md @@ -41,19 +41,15 @@ NDK_PATH="" HOST_PLATFORM="linux-x86_64" ``` -* Fetch FFmpeg: +* Fetch FFmpeg and checkout an appropriate branch. We cannot guarantee + compatibility with all versions of FFmpeg. We currently recommend version 4.2: ``` -cd "${FFMPEG_EXT_PATH}/jni" && \ -git clone git://source.ffmpeg.org/ffmpeg ffmpeg -``` - -* Checkout an appropriate branch of FFmpeg. We cannot guarantee compatibility - with all versions of FFmpeg. We currently recommend version 4.2: - -``` -cd "${FFMPEG_EXT_PATH}/jni/ffmpeg" && \ -git checkout release/4.2 +cd "" && \ +git clone git://source.ffmpeg.org/ffmpeg && \ +cd ffmpeg && \ +git checkout release/4.2 && \ +FFMPEG_PATH="$(pwd)" ``` * Configure the decoders to include. See the [Supported formats][] page for @@ -63,6 +59,13 @@ git checkout release/4.2 ENABLED_DECODERS=(vorbis opus flac) ``` +* Add a link to the FFmpeg source code in the FFmpeg extension `jni` directory. + +``` +cd "${FFMPEG_EXT_PATH}/jni" && \ +ln -s "$FFMPEG_PATH" ffmpeg +``` + * Execute `build_ffmpeg.sh` to build FFmpeg for `armeabi-v7a`, `arm64-v8a`, `x86` and `x86_64`. The script can be edited if you need to build for different architectures: From a15d01d36f8334293b6a1a19d6e66a77994bb696 Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 16 Sep 2020 16:50:50 +0100 Subject: [PATCH 038/110] More verbose instructions for consistency PiperOrigin-RevId: 332015471 --- extensions/ffmpeg/README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/extensions/ffmpeg/README.md b/extensions/ffmpeg/README.md index f3001427b8..639d1f6d6c 100644 --- a/extensions/ffmpeg/README.md +++ b/extensions/ffmpeg/README.md @@ -25,7 +25,8 @@ can bundle the FFmpeg binaries in the APK: ``` cd "" -FFMPEG_EXT_PATH="$(pwd)/extensions/ffmpeg/src/main" +EXOPLAYER_ROOT="$(pwd)" +FFMPEG_EXT_PATH="${EXOPLAYER_ROOT}/extensions/ffmpeg/src/main" ``` * Download the [Android NDK][] and set its location in a shell variable. From 9acf1e8d10d4efdf0c99e5c4c34882956bafb1fd Mon Sep 17 00:00:00 2001 From: bachinger Date: Mon, 21 Sep 2020 14:38:09 +0100 Subject: [PATCH 039/110] Mention moving skipSilence in release note Issue: #7939 PiperOrigin-RevId: 332838586 --- RELEASENOTES.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 966c27ece0..384aac108e 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -39,6 +39,9 @@ * Deprecate `EventListener.onSeekProcessed` because seek changes now happen instantly and listening to `onPositionDiscontinuity` is sufficient. + * Split off `skipSilence' from `PlaybackParameter` and move it + to `AudioComponent.setSkipSilenceEnabled` with callback + `AudioListener.onSkipSilenceEnabledChanged`. * `ExoPlayer`: * Add `setMediaSource(s)` and `addMediaSource(s)` to `ExoPlayer`, for adding `MediaSource` instances directly to the playlist. From d386d03b112f965a77743bfa17d566ab32c3e7c5 Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 21 Sep 2020 22:51:56 +0100 Subject: [PATCH 040/110] Add blog posts to release notes PiperOrigin-RevId: 332940209 --- RELEASENOTES.md | 62 +++++++++++++++++++++++++++++-------------------- 1 file changed, 37 insertions(+), 25 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 384aac108e..aa61bbe0e6 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -2,19 +2,24 @@ ### 2.12.0 (2020-09-11) ### +To learn more about what's new in 2.12, read the corresponding +[blog post](https://medium.com/google-exoplayer/exoplayer-2-12-whats-new-e43ef8ff72e7). + * Core library: * `Player`: * Add a top level playlist API based on a new `MediaItem` class - ([#6161](https://github.com/google/ExoPlayer/issues/6161)). The - new methods for playlist manipulation are `setMediaItem(s)`, + ([#6161](https://github.com/google/ExoPlayer/issues/6161)). The new + methods for playlist manipulation are `setMediaItem(s)`, `addMediaItem(s)`, `moveMediaItem(s)`, `removeMediaItem(s)` and `clearMediaItems`. The playlist can be queried using `getMediaItemCount` and `getMediaItemAt`. This API should be used - instead of `ConcatenatingMediaSource` in most cases. - * Add `getCurrentMediaItem` for getting the currently playing item - in the playlist. - * Add `EventListener.onMediaItemTransition` to report when - playback transitions from one item to another in the playlist. + instead of `ConcatenatingMediaSource` in most cases. Learn more by + reading + [this blog post](https://medium.com/google-exoplayer/a-top-level-playlist-api-for-exoplayer-abe0a24edb55). + * Add `getCurrentMediaItem` for getting the currently playing item in + the playlist. + * Add `EventListener.onMediaItemTransition` to report when playback + transitions from one item to another in the playlist. * Add `play` and `pause` convenience methods. They are equivalent to `setPlayWhenReady(true)` and `setPlayWhenReady(false)` respectively. * Add `getCurrentLiveOffset` for getting the offset of the current @@ -23,6 +28,11 @@ player. * Add `AudioComponent.setAudioSessionId` to set the audio session ID. This method is also available on `SimpleExoPlayer`. + * Remove `PlaybackParameters.skipSilence`, and replace it with + `AudioComponent.setSkipSilenceEnabled`. This method is also + available on `SimpleExoPlayer`. An + `AudioListener.onSkipSilenceEnabledChanged` callback is also + added. * Add `TextComponent.getCurrentCues` to get the current cues. This method is also available on `SimpleExoPlayer`. The current cues are no longer automatically forwarded to a `TextOutput` when it's added @@ -39,9 +49,6 @@ * Deprecate `EventListener.onSeekProcessed` because seek changes now happen instantly and listening to `onPositionDiscontinuity` is sufficient. - * Split off `skipSilence' from `PlaybackParameter` and move it - to `AudioComponent.setSkipSilenceEnabled` with callback - `AudioListener.onSkipSilenceEnabledChanged`. * `ExoPlayer`: * Add `setMediaSource(s)` and `addMediaSource(s)` to `ExoPlayer`, for adding `MediaSource` instances directly to the playlist. @@ -79,8 +86,8 @@ ([#7309](https://github.com/google/ExoPlayer/issues/7309)). `LoadErrorHandlingPolicy` implementations should migrate to implementing the non-deprecated methods of the interface. - * Add an option to `MergingMediaSource` to adjust the time offsets - between the merged sources + * Add an option to `MergingMediaSource` to adjust the time offsets between + the merged sources ([#6103](https://github.com/google/ExoPlayer/issues/6103)). * Move `MediaSourceEventListener.LoadEventInfo` and `MediaSourceEventListener.MediaLoadData` to be top-level classes in @@ -90,11 +97,10 @@ generalize them to work with `Decoder` rather than `SimpleDecoder`. * Deprecate `C.MSG_*` constants, replacing them with constants in `Renderer`. - * Split the `library-core` module into `library-core`, - `library-common` and `library-extractor`. The `library-core` module - has an API dependency on both of the new modules, so this change - should be transparent to developers including ExoPlayer using Gradle - dependencies. + * Split the `library-core` module into `library-core`, `library-common` + and `library-extractor`. The `library-core` module has an API dependency + on both of the new modules, so this change should be transparent to + developers including ExoPlayer using Gradle dependencies. * Add a dependency on Guava. * Video: * Pass frame rate hint to `Surface.setFrameRate` on Android 11. @@ -134,6 +140,10 @@ the `AudioCapabilities` ([#7404](https://github.com/google/ExoPlayer/issues/7404)). * Text: + * Many of the changes described below improve support for Japanese + subtitles. Read + [this blog post](https://medium.com/google-exoplayer/improved-japanese-subtitle-support-7598fee12cf4) + to learn more. * Add a WebView-based output option to `SubtitleView`. This can display some features not supported by the existing Canvas-based output such as vertical text and rubies. It can be enabled by calling @@ -296,8 +306,8 @@ * Analytics: * Extend `EventTime` with more details about the current player state ([#7332](https://github.com/google/ExoPlayer/issues/7332)). - * Add `AnalyticsListener.onVideoFrameProcessingOffset` to report how - early or late video frames are processed relative to them needing to be + * Add `AnalyticsListener.onVideoFrameProcessingOffset` to report how early + or late video frames are processed relative to them needing to be presented. Video frame processing offset fields are also added to `DecoderCounters`. * Fix incorrect `MediaPeriodId` for some renderer errors reported by @@ -307,7 +317,9 @@ * Test utils: Add `TestExoPlayer`, a utility class with APIs to create `SimpleExoPlayer` instances with fake components for testing. * Media2 extension: This is a new extension that makes it easy to use - ExoPlayer together with AndroidX Media2. + ExoPlayer together with AndroidX Media2. Read + [this blog post](https://medium.com/google-exoplayer/the-media2-extension-for-exoplayer-d6b7d89b9063) + to learn more. * Cast extension: Implement playlist API and deprecate the old queue manipulation API. * IMA extension: @@ -337,11 +349,11 @@ * Add `clip_start_position_ms` and `clip_end_position_ms` to allow clipped samples. * Use `StyledPlayerControlView` rather than `PlayerView`. - * Remove support for media tunneling, random ABR and playback of - spherical video. Developers wishing to experiment with these features - can enable them by modifying the demo app source code. - * Add support for downloading DRM-protected content using offline - Widevine licenses. + * Remove support for media tunneling, random ABR and playback of spherical + video. Developers wishing to experiment with these features can enable + them by modifying the demo app source code. + * Add support for downloading DRM-protected content using offline Widevine + licenses. ### 2.11.8 (2020-08-25) ### From 8a0d52be4b5402af0987fc7d009bc49977c72b67 Mon Sep 17 00:00:00 2001 From: tonihei Date: Tue, 22 Sep 2020 13:53:17 +0100 Subject: [PATCH 041/110] Add update to shrinking guide to release notes --- RELEASENOTES.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index aa61bbe0e6..c698c2ee70 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -63,6 +63,10 @@ To learn more about what's new in 2.12, read the corresponding playable `MediaSource` instances. A `DefaultMediaSourceFactory` is used by default. `Builder.setMediaSourceFactory` allows setting a custom factory. + * Update [APK shrinking guide](https://exoplayer.dev/shrinking.html) + to explain how shrinking works with the new `MediaItem` and + `DefaultMediaSourceFactory` implementations + ([#7937](https://github.com/google/ExoPlayer/issues/7937)). * Add additional options to `Builder` that were previously only accessible via setters. * Add opt-in to verify correct thread usage with From 8955cd3a61fe00f2f44f10f62a104175c851d5c0 Mon Sep 17 00:00:00 2001 From: olly Date: Mon, 14 Sep 2020 19:30:48 +0100 Subject: [PATCH 042/110] Clean up experimental offload Javadoc PiperOrigin-RevId: 331591005 --- .../java/com/google/android/exoplayer2/ExoPlayer.java | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayer.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayer.java index b5489186bc..ccb67866a4 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayer.java @@ -16,11 +16,13 @@ package com.google.android.exoplayer2; import android.content.Context; +import android.media.AudioTrack; import android.os.Looper; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; import com.google.android.exoplayer2.analytics.AnalyticsCollector; import com.google.android.exoplayer2.audio.AudioCapabilities; +import com.google.android.exoplayer2.audio.AudioSink; import com.google.android.exoplayer2.audio.DefaultAudioSink; import com.google.android.exoplayer2.audio.MediaCodecAudioRenderer; import com.google.android.exoplayer2.metadata.MetadataRenderer; @@ -622,14 +624,13 @@ public interface ExoPlayer extends Player { * the following: * *

      - *
    • audio offload rendering is enabled in {@link + *
    • Audio offload rendering is enabled in {@link * DefaultRenderersFactory#setEnableAudioOffload} or the equivalent option passed to {@link - * com.google.android.exoplayer2.audio.DefaultAudioSink#DefaultAudioSink(AudioCapabilities, + * DefaultAudioSink#DefaultAudioSink(AudioCapabilities, * DefaultAudioSink.AudioProcessorChain, boolean, boolean, boolean)}. - *
    • an audio track is playing in a format which the device supports offloading (for example, + *
    • An audio track is playing in a format that the device supports offloading (for example, * MP3 or AAC). - *
    • The {@link com.google.android.exoplayer2.audio.AudioSink} is playing with an offload - * {@link android.media.AudioTrack}. + *
    • The {@link AudioSink} is playing with an offload {@link AudioTrack}. *
    * *

    This method is experimental, and will be renamed or removed in a future release. From 97a0df77f6932f1a65f4bbe0eb2f800a47d84f9b Mon Sep 17 00:00:00 2001 From: kimvde Date: Wed, 9 Sep 2020 17:23:05 +0100 Subject: [PATCH 043/110] Support android.resource URI scheme Issue: #7866 PiperOrigin-RevId: 330736774 --- RELEASENOTES.md | 6 ++ .../upstream/DefaultDataSource.java | 10 ++- .../upstream/RawResourceDataSource.java | 90 +++++++++++++------ 3 files changed, 78 insertions(+), 28 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index aa61bbe0e6..61a4a6900c 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -1,5 +1,11 @@ # Release notes +### 2.12.1 ### + +* Data sources: + * Add support for `android.resource` URI scheme in `RawResourceDataSource` + ([#7866](https://github.com/google/ExoPlayer/issues/7866)). + ### 2.12.0 (2020-09-11) ### To learn more about what's new in 2.12, read the corresponding diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultDataSource.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultDataSource.java index 7efa89eaa0..12fea3898c 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultDataSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/DefaultDataSource.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2.upstream; +import android.content.ContentResolver; import android.content.Context; import android.net.Uri; import androidx.annotation.Nullable; @@ -39,6 +40,9 @@ import java.util.Map; *

  • rawresource: For fetching data from a raw resource in the application's apk (e.g. * rawresource:///resourceId, where rawResourceId is the integer identifier of the raw * resource). + *
  • android.resource: For fetching data in the application's apk (e.g. + * android.resource:///resourceId or android.resource://resourceType/resourceName). See {@link + * RawResourceDataSource} for more information about the URI form. *
  • content: For fetching data from a content URI (e.g. content://authority/path/123). *
  • rtmp: For fetching data over RTMP. Only supported if the project using ExoPlayer has an * explicit dependency on ExoPlayer's RTMP extension. @@ -58,7 +62,9 @@ public final class DefaultDataSource implements DataSource { private static final String SCHEME_CONTENT = "content"; private static final String SCHEME_RTMP = "rtmp"; private static final String SCHEME_UDP = "udp"; + private static final String SCHEME_DATA = DataSchemeDataSource.SCHEME_DATA; private static final String SCHEME_RAW = RawResourceDataSource.RAW_RESOURCE_SCHEME; + private static final String SCHEME_ANDROID_RESOURCE = ContentResolver.SCHEME_ANDROID_RESOURCE; private final Context context; private final List transferListeners; @@ -182,9 +188,9 @@ public final class DefaultDataSource implements DataSource { dataSource = getRtmpDataSource(); } else if (SCHEME_UDP.equals(scheme)) { dataSource = getUdpDataSource(); - } else if (DataSchemeDataSource.SCHEME_DATA.equals(scheme)) { + } else if (SCHEME_DATA.equals(scheme)) { dataSource = getDataSchemeDataSource(); - } else if (SCHEME_RAW.equals(scheme)) { + } else if (SCHEME_RAW.equals(scheme) || SCHEME_ANDROID_RESOURCE.equals(scheme)) { dataSource = getRawResourceDataSource(); } else { dataSource = baseDataSource; diff --git a/library/core/src/main/java/com/google/android/exoplayer2/upstream/RawResourceDataSource.java b/library/core/src/main/java/com/google/android/exoplayer2/upstream/RawResourceDataSource.java index 0595cb84bc..7538cc67a4 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/upstream/RawResourceDataSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/upstream/RawResourceDataSource.java @@ -18,6 +18,7 @@ package com.google.android.exoplayer2.upstream; import static com.google.android.exoplayer2.util.Util.castNonNull; import static java.lang.Math.min; +import android.content.ContentResolver; import android.content.Context; import android.content.res.AssetFileDescriptor; import android.content.res.Resources; @@ -34,9 +35,20 @@ import java.io.InputStream; /** * A {@link DataSource} for reading a raw resource inside the APK. * - *

    URIs supported by this source are of the form {@code rawresource:///rawResourceId}, where - * rawResourceId is the integer identifier of a raw resource. {@link #buildRawResourceUri(int)} can - * be used to build {@link Uri}s in this format. + *

    URIs supported by this source are of one of the forms: + * + *

      + *
    • {@code rawresource:///id}, where {@code id} is the integer identifier of a raw resource. + *
    • {@code android.resource:///id}, where {@code id} is the integer identifier of a raw + * resource. + *
    • {@code android.resource://[package]/[type/]name}, where {@code package} is the name of the + * package in which the resource is located, {@code type} is the resource type and {@code + * name} is the resource name. The package and the type are optional. Their default value is + * the package of this application and "raw", respectively. Using the two other forms is more + * efficient. + *
    + * + *

    {@link #buildRawResourceUri(int)} can be used to build supported {@link Uri}s. */ public final class RawResourceDataSource extends BaseDataSource { @@ -67,6 +79,7 @@ public final class RawResourceDataSource extends BaseDataSource { public static final String RAW_RESOURCE_SCHEME = "rawresource"; private final Resources resources; + private final String packageName; @Nullable private Uri uri; @Nullable private AssetFileDescriptor assetFileDescriptor; @@ -80,33 +93,55 @@ public final class RawResourceDataSource extends BaseDataSource { public RawResourceDataSource(Context context) { super(/* isNetwork= */ false); this.resources = context.getResources(); + this.packageName = context.getPackageName(); } @Override public long open(DataSpec dataSpec) throws RawResourceDataSourceException { - try { - Uri uri = dataSpec.uri; - this.uri = uri; - if (!TextUtils.equals(RAW_RESOURCE_SCHEME, uri.getScheme())) { - throw new RawResourceDataSourceException("URI must use scheme " + RAW_RESOURCE_SCHEME); - } + Uri uri = dataSpec.uri; + this.uri = uri; - int resourceId; + int resourceId; + if (TextUtils.equals(RAW_RESOURCE_SCHEME, uri.getScheme()) + || (TextUtils.equals(ContentResolver.SCHEME_ANDROID_RESOURCE, uri.getScheme()) + && uri.getPathSegments().size() == 1 + && Assertions.checkNotNull(uri.getLastPathSegment()).matches("\\d+"))) { try { resourceId = Integer.parseInt(Assertions.checkNotNull(uri.getLastPathSegment())); } catch (NumberFormatException e) { throw new RawResourceDataSourceException("Resource identifier must be an integer."); } - - transferInitializing(dataSpec); - AssetFileDescriptor assetFileDescriptor = resources.openRawResourceFd(resourceId); - this.assetFileDescriptor = assetFileDescriptor; - if (assetFileDescriptor == null) { - throw new RawResourceDataSourceException("Resource is compressed: " + uri); + } else if (TextUtils.equals(ContentResolver.SCHEME_ANDROID_RESOURCE, uri.getScheme())) { + String path = Assertions.checkNotNull(uri.getPath()); + if (path.startsWith("/")) { + path = path.substring(1); } - FileInputStream inputStream = new FileInputStream(assetFileDescriptor.getFileDescriptor()); - this.inputStream = inputStream; + @Nullable String host = uri.getHost(); + String resourceName = (TextUtils.isEmpty(host) ? "" : (host + ":")) + path; + resourceId = + resources.getIdentifier( + resourceName, /* defType= */ "raw", /* defPackage= */ packageName); + if (resourceId == 0) { + throw new RawResourceDataSourceException("Resource not found."); + } + } else { + throw new RawResourceDataSourceException( + "URI must either use scheme " + + RAW_RESOURCE_SCHEME + + " or " + + ContentResolver.SCHEME_ANDROID_RESOURCE); + } + transferInitializing(dataSpec); + AssetFileDescriptor assetFileDescriptor = resources.openRawResourceFd(resourceId); + this.assetFileDescriptor = assetFileDescriptor; + if (assetFileDescriptor == null) { + throw new RawResourceDataSourceException("Resource is compressed: " + uri); + } + + FileInputStream inputStream = new FileInputStream(assetFileDescriptor.getFileDescriptor()); + this.inputStream = inputStream; + try { inputStream.skip(assetFileDescriptor.getStartOffset()); long skipped = inputStream.skip(dataSpec.position); if (skipped < dataSpec.position) { @@ -114,18 +149,21 @@ public final class RawResourceDataSource extends BaseDataSource { // skip beyond the end of the data. throw new EOFException(); } - if (dataSpec.length != C.LENGTH_UNSET) { - bytesRemaining = dataSpec.length; - } else { - long assetFileDescriptorLength = assetFileDescriptor.getLength(); - // If the length is UNKNOWN_LENGTH then the asset extends to the end of the file. - bytesRemaining = assetFileDescriptorLength == AssetFileDescriptor.UNKNOWN_LENGTH - ? C.LENGTH_UNSET : (assetFileDescriptorLength - dataSpec.position); - } } catch (IOException e) { throw new RawResourceDataSourceException(e); } + if (dataSpec.length != C.LENGTH_UNSET) { + bytesRemaining = dataSpec.length; + } else { + long assetFileDescriptorLength = assetFileDescriptor.getLength(); + // If the length is UNKNOWN_LENGTH then the asset extends to the end of the file. + bytesRemaining = + assetFileDescriptorLength == AssetFileDescriptor.UNKNOWN_LENGTH + ? C.LENGTH_UNSET + : (assetFileDescriptorLength - dataSpec.position); + } + opened = true; transferStarted(dataSpec); From f4896d769f661cc6d6c376e593c0e310c0b01b2a Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 16 Sep 2020 16:43:45 +0100 Subject: [PATCH 044/110] Replace duration strings with plurals PiperOrigin-RevId: 332014290 --- .../exoplayer2/ui/StyledPlayerControlView.java | 13 ++++++++----- library/ui/src/main/res/values/strings.xml | 10 ++++++++-- library/ui/src/main/res/values/styles.xml | 2 -- 3 files changed, 16 insertions(+), 9 deletions(-) diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/StyledPlayerControlView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/StyledPlayerControlView.java index 97652ad01f..8bb9babeb0 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/StyledPlayerControlView.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/StyledPlayerControlView.java @@ -1159,13 +1159,14 @@ public class StyledPlayerControlView extends FrameLayout { if (controlDispatcher instanceof DefaultControlDispatcher) { rewindMs = ((DefaultControlDispatcher) controlDispatcher).getRewindIncrementMs(); } - long rewindSec = rewindMs / 1_000; + int rewindSec = (int) (rewindMs / 1_000); if (rewindButtonTextView != null) { rewindButtonTextView.setText(String.valueOf(rewindSec)); } if (rewindButton != null) { rewindButton.setContentDescription( - resources.getString(R.string.exo_controls_rewind_by_amount_description, rewindSec)); + resources.getQuantityString( + R.plurals.exo_controls_rewind_by_amount_description, rewindSec, rewindSec)); } } @@ -1173,14 +1174,16 @@ public class StyledPlayerControlView extends FrameLayout { if (controlDispatcher instanceof DefaultControlDispatcher) { fastForwardMs = ((DefaultControlDispatcher) controlDispatcher).getFastForwardIncrementMs(); } - long fastForwardSec = fastForwardMs / 1_000; + int fastForwardSec = (int) (fastForwardMs / 1_000); if (fastForwardButtonTextView != null) { fastForwardButtonTextView.setText(String.valueOf(fastForwardSec)); } if (fastForwardButton != null) { fastForwardButton.setContentDescription( - resources.getString( - R.string.exo_controls_fastforward_by_amount_description, fastForwardSec)); + resources.getQuantityString( + R.plurals.exo_controls_fastforward_by_amount_description, + fastForwardSec, + fastForwardSec)); } } diff --git a/library/ui/src/main/res/values/strings.xml b/library/ui/src/main/res/values/strings.xml index a65d81e2b1..a11d04073f 100644 --- a/library/ui/src/main/res/values/strings.xml +++ b/library/ui/src/main/res/values/strings.xml @@ -43,11 +43,17 @@ Rewind - Rewind %d seconds + + Rewind %d second + Rewind %d seconds + Fast forward - Fast forward %d seconds + + Fast forward %d second + Fast forward %d seconds + Repeat none diff --git a/library/ui/src/main/res/values/styles.xml b/library/ui/src/main/res/values/styles.xml index 76ea27ef5a..f903441698 100644 --- a/library/ui/src/main/res/values/styles.xml +++ b/library/ui/src/main/res/values/styles.xml @@ -90,14 +90,12 @@ - From 6a9f125c149a93851aa970cd762fb837814a41b4 Mon Sep 17 00:00:00 2001 From: tonihei Date: Tue, 22 Sep 2020 09:43:23 +0100 Subject: [PATCH 051/110] Don't assume FakeSampleStream is ended without end of stream signal PiperOrigin-RevId: 333029935 --- .../android/exoplayer2/testutil/FakeSampleStream.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeSampleStream.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeSampleStream.java index 7d63e129db..4f4aee9675 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeSampleStream.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeSampleStream.java @@ -190,7 +190,12 @@ public class FakeSampleStream implements SampleStream { } } sampleItemIndex = fakeSampleStreamItems.size(); - readEOSBuffer = true; + if (!fakeSampleStreamItems.isEmpty()) { + FakeSampleStreamItem lastItem = Iterables.getLast(fakeSampleStreamItems); + readEOSBuffer = + lastItem.sampleInfo != null + && ((lastItem.sampleInfo.flags & C.BUFFER_FLAG_END_OF_STREAM) != 0); + } } /** From 1b5d07c10536bc64a78b38796e958f9e8a0da423 Mon Sep 17 00:00:00 2001 From: tonihei Date: Tue, 22 Sep 2020 09:56:14 +0100 Subject: [PATCH 052/110] Guava-ify https://github.com/google/ExoPlayer/commit/f2c51560c21bdd757c30678223345fa8f59fb82b PiperOrigin-RevId: 333031301 --- .../exoplayer2/testutil/FakeSampleStream.java | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeSampleStream.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeSampleStream.java index 4f4aee9675..eaa2fb52bb 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeSampleStream.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeSampleStream.java @@ -190,12 +190,13 @@ public class FakeSampleStream implements SampleStream { } } sampleItemIndex = fakeSampleStreamItems.size(); - if (!fakeSampleStreamItems.isEmpty()) { - FakeSampleStreamItem lastItem = Iterables.getLast(fakeSampleStreamItems); - readEOSBuffer = - lastItem.sampleInfo != null - && ((lastItem.sampleInfo.flags & C.BUFFER_FLAG_END_OF_STREAM) != 0); - } + @Nullable + FakeSampleStreamItem lastItem = + Iterables.getLast(fakeSampleStreamItems, /* defaultValue= */ null); + readEOSBuffer = + lastItem != null + && lastItem.sampleInfo != null + && ((lastItem.sampleInfo.flags & C.BUFFER_FLAG_END_OF_STREAM) != 0); } /** From b6036561c27284bc618fb5e8b6ca0497328a91d5 Mon Sep 17 00:00:00 2001 From: ibaker Date: Tue, 22 Sep 2020 09:57:16 +0100 Subject: [PATCH 053/110] Exclude PC devices from H.265 GTS tests PiperOrigin-RevId: 333031399 --- .../playbacktests/gts/DashStreamingTest.java | 24 ++++++++++++------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashStreamingTest.java b/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashStreamingTest.java index a2f557ca0d..c3e82ec33d 100644 --- a/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashStreamingTest.java +++ b/playbacktests/src/androidTest/java/com/google/android/exoplayer2/playbacktests/gts/DashStreamingTest.java @@ -18,6 +18,7 @@ package com.google.android.exoplayer2.playbacktests.gts; import static com.google.android.exoplayer2.playbacktests.gts.GtsTestUtil.shouldSkipWidevineTest; import static com.google.common.truth.Truth.assertThat; +import android.content.pm.PackageManager; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.rule.ActivityTestRule; import com.google.android.exoplayer2.ExoPlayer; @@ -168,7 +169,7 @@ public final class DashStreamingTest { @Test public void h265FixedV23() throws Exception { - if (Util.SDK_INT < 23) { + if (Util.SDK_INT < 23 || isPc()) { // Pass. return; } @@ -183,7 +184,7 @@ public final class DashStreamingTest { @Test public void h265AdaptiveV24() throws Exception { - if (Util.SDK_INT < 24) { + if (Util.SDK_INT < 24 || isPc()) { // Pass. return; } @@ -199,7 +200,7 @@ public final class DashStreamingTest { @Test public void h265AdaptiveWithSeekingV24() throws Exception { - if (Util.SDK_INT < 24) { + if (Util.SDK_INT < 24 || isPc()) { // Pass. return; } @@ -216,7 +217,7 @@ public final class DashStreamingTest { @Test public void h265AdaptiveWithRendererDisablingV24() throws Exception { - if (Util.SDK_INT < 24) { + if (Util.SDK_INT < 24 || isPc()) { // Pass. return; } @@ -435,7 +436,7 @@ public final class DashStreamingTest { @Test public void widevineH265FixedV23() throws Exception { - if (Util.SDK_INT < 23 || GtsTestUtil.shouldSkipWidevineTest(testRule.getActivity())) { + if (Util.SDK_INT < 23 || GtsTestUtil.shouldSkipWidevineTest(testRule.getActivity()) || isPc()) { // Pass. return; } @@ -452,7 +453,7 @@ public final class DashStreamingTest { @Test public void widevineH265AdaptiveV24() throws Exception { - if (Util.SDK_INT < 24 || GtsTestUtil.shouldSkipWidevineTest(testRule.getActivity())) { + if (Util.SDK_INT < 24 || GtsTestUtil.shouldSkipWidevineTest(testRule.getActivity()) || isPc()) { // Pass. return; } @@ -469,7 +470,7 @@ public final class DashStreamingTest { @Test public void widevineH265AdaptiveWithSeekingV24() throws Exception { - if (Util.SDK_INT < 24 || GtsTestUtil.shouldSkipWidevineTest(testRule.getActivity())) { + if (Util.SDK_INT < 24 || GtsTestUtil.shouldSkipWidevineTest(testRule.getActivity()) || isPc()) { // Pass. return; } @@ -487,7 +488,7 @@ public final class DashStreamingTest { @Test public void widevineH265AdaptiveWithRendererDisablingV24() throws Exception { - if (Util.SDK_INT < 24 || GtsTestUtil.shouldSkipWidevineTest(testRule.getActivity())) { + if (Util.SDK_INT < 24 || GtsTestUtil.shouldSkipWidevineTest(testRule.getActivity()) || isPc()) { // Pass. return; } @@ -644,7 +645,7 @@ public final class DashStreamingTest { @Test public void decoderInfoH265V24() throws Exception { - if (Util.SDK_INT < 24) { + if (Util.SDK_INT < 24 || isPc()) { // Pass. return; } @@ -670,6 +671,11 @@ public final class DashStreamingTest { // Internal. + private boolean isPc() { + // See [internal b/162990153]. + return testRule.getActivity().getPackageManager().hasSystemFeature(PackageManager.FEATURE_PC); + } + private static boolean shouldSkipAdaptiveTest(String mimeType) throws DecoderQueryException { MediaCodecInfo decoderInfo = MediaCodecUtil.getDecoderInfo(mimeType, /* secure= */ false, /* tunneling= */ false); From 93948471c8d870cabdced0526ec398f09f753641 Mon Sep 17 00:00:00 2001 From: tonihei Date: Tue, 22 Sep 2020 12:23:24 +0100 Subject: [PATCH 054/110] Don't require the existence of the next period to wait for its stream. We have a workaround for uneven sample stream durarions in playlists that assumes a renderer allows playback if it's reading ahead or waiting for the next stream. https://github.com/google/ExoPlayer/commit/652c2f9c188bf9d9d6e323ff5333e5026454a082 changed this logic to no longer require to wait until the next stream is prepared due to a change in how we advance media periods in the queue. However, the code falsely still requires the next stream to exist (even if it's not prepared). This can cause a stuck buffering state when the difference in the duration of the streams is more than what we buffer ahead because we never create the next stream in such a case. Note: DefaultMediaClock.shouldUseStandaloneClock has roughly the same logic and also doesn't require the next stream to be present. Also fix a test that seemed to rely on this stuck buffering case to test stuck buffering detection. Changed the test to not read the end of stream to ensure it runs into the desired stuck buffering case. Issue:#7943 PiperOrigin-RevId: 333050285 --- RELEASENOTES.md | 4 ++++ .../android/exoplayer2/ExoPlayerImplInternal.java | 5 +---- .../google/android/exoplayer2/ExoPlayerTest.java | 15 +++++++-------- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 6c382f8724..dbeb77236a 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -2,6 +2,10 @@ ### 2.12.1 ### +* Core library: + * Fix bug where streams with highly uneven durations may get stuck in a + buffering state + ([#7943](https://github.com/google/ExoPlayer/issues/7943)). * Data sources: * Add support for `android.resource` URI scheme in `RawResourceDataSource` ([#7866](https://github.com/google/ExoPlayer/issues/7866)). diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java index 9739680e79..e33b93ac0e 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImplInternal.java @@ -897,10 +897,7 @@ import java.util.concurrent.atomic.AtomicBoolean; // tracks in the current period have uneven durations and are still being read by another // renderer. See: https://github.com/google/ExoPlayer/issues/1874. boolean isReadingAhead = playingPeriodHolder.sampleStreams[i] != renderer.getStream(); - boolean isWaitingForNextStream = - !isReadingAhead - && playingPeriodHolder.getNext() != null - && renderer.hasReadStreamToEnd(); + boolean isWaitingForNextStream = !isReadingAhead && renderer.hasReadStreamToEnd(); boolean allowsPlayback = isReadingAhead || isWaitingForNextStream || renderer.isReady() || renderer.isEnded(); renderersAllowPlayback = renderersAllowPlayback && allowsPlayback; diff --git a/library/core/src/test/java/com/google/android/exoplayer2/ExoPlayerTest.java b/library/core/src/test/java/com/google/android/exoplayer2/ExoPlayerTest.java index 444640256f..7934298df0 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/ExoPlayerTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/ExoPlayerTest.java @@ -7334,6 +7334,8 @@ public final class ExoPlayerTest { new DefaultLoadControl.Builder() .setTargetBufferBytes(10 * C.DEFAULT_BUFFER_SEGMENT_SIZE) .build(); + // Return no end of stream signal to prevent playback from ending. + FakeMediaPeriod.TrackDataFactory trackDataWithoutEos = (format, periodId) -> ImmutableList.of(); MediaSource continuouslyAllocatingMediaSource = new FakeMediaSource( new FakeTimeline(/* windowCount= */ 1), ExoPlayerTestRunner.VIDEO_FORMAT) { @@ -7348,8 +7350,11 @@ public final class ExoPlayerTest { @Nullable TransferListener transferListener) { return new FakeMediaPeriod( trackGroupArray, - TimelineWindowDefinition.DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US, - mediaSourceEventDispatcher) { + trackDataWithoutEos, + mediaSourceEventDispatcher, + drmSessionManager, + drmEventDispatcher, + /* deferOnPrepared= */ false) { private final List allocations = new ArrayList<>(); @@ -7382,14 +7387,8 @@ public final class ExoPlayerTest { }; } }; - ActionSchedule actionSchedule = - new ActionSchedule.Builder(TAG) - // Prevent player from ever assuming it finished playing. - .setRepeatMode(Player.REPEAT_MODE_ALL) - .build(); ExoPlayerTestRunner testRunner = new ExoPlayerTestRunner.Builder(context) - .setActionSchedule(actionSchedule) .setMediaSources(continuouslyAllocatingMediaSource) .setLoadControl(loadControl) .build(); From 884a0f52cd91d70be6fdfd06b3229842cdf7eb34 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Tue, 22 Sep 2020 12:27:58 +0100 Subject: [PATCH 055/110] Remove unused MP4 atom type PiperOrigin-RevId: 333051018 --- .../java/com/google/android/exoplayer2/extractor/mp4/Atom.java | 3 --- 1 file changed, 3 deletions(-) diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mp4/Atom.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mp4/Atom.java index e86a873ed5..91b26562ca 100644 --- a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mp4/Atom.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mp4/Atom.java @@ -274,9 +274,6 @@ import java.util.List; @SuppressWarnings("ConstantCaseForConstants") public static final int TYPE_TTML = 0x54544d4c; - @SuppressWarnings("ConstantCaseForConstants") - public static final int TYPE_vmhd = 0x766d6864; - @SuppressWarnings("ConstantCaseForConstants") public static final int TYPE_mp4v = 0x6d703476; From 79638d1e3d870125b7f0ea67c93985e96d0d1f33 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Wed, 23 Sep 2020 13:22:27 +0100 Subject: [PATCH 056/110] Add support for 'mett' sample description PiperOrigin-RevId: 333272292 --- .../android/exoplayer2/extractor/mp4/Atom.java | 3 +++ .../exoplayer2/extractor/mp4/AtomParsers.java | 14 ++++++++++++++ 2 files changed, 17 insertions(+) diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mp4/Atom.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mp4/Atom.java index 91b26562ca..58f3a75b87 100644 --- a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mp4/Atom.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mp4/Atom.java @@ -355,6 +355,9 @@ import java.util.List; @SuppressWarnings("ConstantCaseForConstants") public static final int TYPE_camm = 0x63616d6d; + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE_mett = 0x6d657474; + @SuppressWarnings("ConstantCaseForConstants") public static final int TYPE_alac = 0x616c6163; diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java index 6eed09760e..0ab126367b 100644 --- a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java @@ -891,6 +891,8 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; || childAtomType == Atom.TYPE_c608) { parseTextSampleEntry(stsd, childAtomType, childStartPosition, childAtomSize, trackId, language, out); + } else if (childAtomType == Atom.TYPE_mett) { + parseMetaDataSampleEntry(stsd, childAtomType, childStartPosition, trackId, out); } else if (childAtomType == Atom.TYPE_camm) { out.format = new Format.Builder() @@ -1097,6 +1099,18 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; .build(); } + private static void parseMetaDataSampleEntry( + ParsableByteArray parent, int atomType, int position, int trackId, StsdData out) { + parent.setPosition(position + Atom.HEADER_SIZE + StsdData.STSD_HEADER_SIZE); + if (atomType == Atom.TYPE_mett) { + parent.readNullTerminatedString(); // Skip optional content_encoding + @Nullable String mimeType = parent.readNullTerminatedString(); + if (mimeType != null) { + out.format = new Format.Builder().setId(trackId).setSampleMimeType(mimeType).build(); + } + } + } + /** * Parses the edts atom (defined in ISO/IEC 14496-12 subsection 8.6.5). * From df1619fa3f37ac1e6568097c0e05403fcbbddb5f Mon Sep 17 00:00:00 2001 From: ibaker Date: Wed, 23 Sep 2020 16:27:24 +0100 Subject: [PATCH 057/110] Add Japanese subtitle examples to the demo app These are from https://medium.com/google-exoplayer/improved-japanese-subtitle-support-7598fee12cf4 PiperOrigin-RevId: 333296789 --- demos/main/src/main/assets/media.exolist.json | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/demos/main/src/main/assets/media.exolist.json b/demos/main/src/main/assets/media.exolist.json index ce1854db85..24213918f5 100644 --- a/demos/main/src/main/assets/media.exolist.json +++ b/demos/main/src/main/assets/media.exolist.json @@ -527,6 +527,20 @@ { "name": "MPEG-4 Timed Text (tx3g, mov_text)", "uri": "https://storage.googleapis.com/exoplayer-test-media-1/mp4/dizzy-with-tx3g.mp4" + }, + { + "name": "Japanese features (vertical + rubies) [TTML]", + "uri": "https://html5demos.com/assets/dizzy.mp4", + "subtitle_uri": "https://storage.googleapis.com/exoplayer-test-media-1/ttml/japanese-ttml.xml", + "subtitle_mime_type": "application/ttml+xml", + "subtitle_language": "ja" + }, + { + "name": "Japanese features (vertical + rubies) [WebVTT]", + "uri": "https://html5demos.com/assets/dizzy.mp4", + "subtitle_uri": "https://storage.googleapis.com/exoplayer-test-media-1/webvtt/japanese.vtt", + "subtitle_mime_type": "text/vtt", + "subtitle_language": "ja" } ] }, From 92f9de8ea3a0f217864e79deb8a584d1c29fb309 Mon Sep 17 00:00:00 2001 From: ibaker Date: Thu, 24 Sep 2020 10:00:46 +0100 Subject: [PATCH 058/110] Delete sample_cbs.adts test asset This seems to be an exact copy of sample.adts. Update the test to use the same sample but just output to a different dump file. PiperOrigin-RevId: 333469714 --- .../extractor/ts/AdtsExtractorTest.java | 6 +++++- .../src/test/assets/media/ts/sample_cbs.adts | Bin 31805 -> 0 bytes 2 files changed, 5 insertions(+), 1 deletion(-) delete mode 100644 testdata/src/test/assets/media/ts/sample_cbs.adts diff --git a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/ts/AdtsExtractorTest.java b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/ts/AdtsExtractorTest.java index e8bc727222..dca8ba9938 100644 --- a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/ts/AdtsExtractorTest.java +++ b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/ts/AdtsExtractorTest.java @@ -16,6 +16,7 @@ package com.google.android.exoplayer2.extractor.ts; import com.google.android.exoplayer2.testutil.ExtractorAsserts; +import com.google.android.exoplayer2.testutil.ExtractorAsserts.AssertionConfig; import java.util.List; import org.junit.Test; import org.junit.runner.RunWith; @@ -49,7 +50,10 @@ public final class AdtsExtractorTest { public void sample_withSeeking() throws Exception { ExtractorAsserts.assertBehavior( () -> new AdtsExtractor(/* flags= */ AdtsExtractor.FLAG_ENABLE_CONSTANT_BITRATE_SEEKING), - "media/ts/sample_cbs.adts", + "media/ts/sample.adts", + new AssertionConfig.Builder() + .setDumpFilesPrefix("extractordumps/ts/sample_cbs.adts") + .build(), simulationConfig); } diff --git a/testdata/src/test/assets/media/ts/sample_cbs.adts b/testdata/src/test/assets/media/ts/sample_cbs.adts deleted file mode 100644 index abbaad0daf95cf2bce299b161a2ea6d658b587e0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 31805 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∈f1 zT{6X+uap!9TcvXCyyk&OV%%uM-KlIVflqVvzx|%{alem3HhSKyx)M_z%VX#{jm<=7$*xYxDsq>4>NOy{)0&jkj=o{6d9 zsqlkRI|8*m{kI&XW_vu5QoWMxyQ2j#7l^4=WJxi1dVUK>;jspFX`TI7v*7y?@13!3 zY;k}H!{612B66zwZA+cP1+RPl4)ZtLGxNpkn`XWo#icVQ3?jc;VIH7xo^J!%M2Aq($LDg#iW)k8}zU?aRPtFHzUPZCJOI(!UmDILA9~@b*9k7eL zWWP&0{}+lwh`#RhSg}-yPK-(PsC(f(VnuwQN01sbnm^3=h(^VT6mB%*8`2R`zpQbRe1%i_Qc|{jbmzt!-IjwWA~A0zUz)jZyIY;x zjsZAh$oveSiXjIMXx%VNG?5RPxp%=45~3XRAw&+K&f5e=Slb($N7sL2Ei@1D@L~}o zWcIvO-FoW-dFvaGd*no69+rNoeA*B8d7-aevK0Lm^`JD`Sn|K=nZTO&%R4{c z&hM2qr;GQK;(u`WH8{WfTBol4EKApN*&S5=mF$(mwQ5q;(aOU}^Rl!v7tjY`bW?F8 z@pdphUfGYe2PN4~wmeHJ(lLIqCYMe8MdG+2&-a*G@^CdvA7mY%S5qn$CZW3*7|`pP zAD+l#+}vy-yF15VltFP8DWx)JAXp$P=&N;ExAf{f-#3np))_#B)$fy~M4((mqy?F5J> zlr&H%DpCrUX3vzTzD?dS?B|VN>31yh-_E5_Y6DBoAa-fRKX$K5V9?2=wG-1 zl|g%CGS|O+tl%3=!f9xq&rF>0=6GLw&1dF_6*NJ#G0HmTG{3-ZQ(G=IXY#z2`b7hX?;|aq2SJAjV6$DN{>v@I-q}}MWgD9 zdzwu?2Bp5et=Z$mbS$gA^CaQ86dqWu}{eM{7{wCkyj<(yf zv`CT!9kkO){xs=)3NIz-k=Vo`G$mRz>5n-d*=1SKP^1aP2eg6Cx}+vpO9_lM9xY56 z!J59cw`)UNdvkY-KkJYB>+(FQudfmpYPO)0i4uz-hyo@kR99;oqgQf-BbeP4d3VGOxer8$Eh%OJAP?4s{T^LS>K~BYSHns1?W9* z4FB8w(X`OoQ)BHusZvYXI&(XW;z}u&uG6&yZ#=C{DBGmXL|B|cYE;7##4j?S_JzzQ zU-gvv)Kz|JJfw*^X8zZ}Z2f#YM5GT(gReKCm|1@6V==|kUIBq3oePv`kmU?JKVB7Ft#O&1{+;&8eSVU1ZkJ+-`jMr#c|i3oEOCPQ4!jge@QP z&k8jR_`Qw3nH1d`iQKF)awg7nrY3U2MukNo9c}%`r&Scf{R&YRU84LD_T{u&-pkL0 zvC4B794)P1bCnqu!st8Yh0ETt7|x5WU?y~V4`<_K`>Seo>#I`_#LAnp30oH7Aoa4hKj`BJ1xUJ zhv2vE)cYV)gUpy=t^1A{yZ+Gcegft7UeI@aW9q`rOIZT*eu@j1J2pU(&LQ`%g;&Z4 zYFu_Rb-X8wT45`a8sC!Kx=#NYc%(FgrpkrK80JP#T07Dl@)n`$+Yp%Ua{^sl@>qX@ zZPoBw>pxbulCA{bk$%pt&5Jz+1C#(4jJP6 zQ$0o2S+U#yKL1D&i{TAHs$xP&`wuqGm+oR8|C3$dkFrZ9J1gOfFl6f=;pX@l;k~q8 zLy)W)6BE{aQ_e~yO_siYt|m4jRN{wsnNHcw+xDf~V|$4KV4M|WjCg1LW6LkxN~*oSwnGQ&1ufO_@T+j( zui?4i>9!2Ha?Qy}wlL#TK(syu)G@)k^c>ylBIi<2`EQoP#-Nv3zNs_d)ISGNS+f!u@`e2QNh03{Pxn(I|be#on$Z&C^Y;G*we*Rc!m}#;5uQ z7+C|KzZ3Kkefaz!zD(zlr3F*rfQu(J4PLqC?{zIM*{+>Jp^zxLg@=Gvo0E--hv&bb z3yxEwC+GVAz6CVd_9b(~-_*J7@3f;L%O#Yx+*NNk?rD1hhoDBzR_4nKj2ou`G1ibK zd9L1D3oPSn?Ve0_9@W)R+-;eg5pVAh){HM4epgt=4Rx=I#z3a3N{xMCww|{0>(O_j*&6e0?fye+q#r>mBiSSX&uje{J+f~B^c&(8 zm(NCd3p|wOeI{YCpZ2{s9N*S`dG{Sa}SD2D2F>d`34FhZCh~C6dUvvIV~eY_Gj}H; z0imQiDt>6a+{p^K@wQ2Sfb|8;_ct5y05s?ufm~~a8hwNhSDYpU`gOB-v=uTi#(PU54X<6;z zUmD9vP=Jyf>VI4Z{8*?ts*i`mUx)*ZDk(XF^gJqQgm`GEW^ck3rDd?XohvO@rntP* zRb>-Y{#3AR2_sJ{4c}fw7oR=AJB}(Jq9j(^G|094C2&oJOGBPZ)m|y^gI5LaSfLwv zmL=?M>Dp19Gq;md*r*meFglh8zte2(Gu*-tCGqV=q53Aep|7*PI7oyXfuLM15K1`jC-@5MdXp@_y536^F?`a}I4;iiaOeFU^x#ZQ`r? z7D=jKXuq^q^!*LgX};W~MIj6>NPp=~vYgE(Ss#R%QZPg8U;QNUQ%fLDZ@RCZj-_jl zDibLdq8CfYV@u;>AnwBcWd_9?tQPGvzZvaJZS~cgKFmqT=qsI08G_Qhp?tTV4&HCm z#YE-=)01Ma!b;-@hfsj)yxH@t_#NP77}x%MU+P#h2?#aqCtiv4q-X^*SNN zQNCVXQKH}gN68w1ZCD0P#-eCVt+>8_&RopWq=`9r-0Jg^e-XwfFA&mj|Jn5tlYyLVG9A! zj)#jvsbZfZZ|nUZBAfk1VwumPOqQ;?M`!Bc)ccfe_nUW%Lp?)bqLt9yTt7=+rW07z z+*FgobrxQ!vqgpn_I#s`CAiq;OxNILk=8JuXq9|SLf}{IGYvks`ba^*(81;Ty)XD) ziVAJVj>Z==}Pz}y$M*Ch!$BZszL5+g%e5-A)&#nYW3!d3<6^>)K|F%(l z+dUviU$xj+&r&FMR_fEkF~Bn*30YY^Bk(CDsHh*?IpV^OjTqGCUEjG0u@KU$%&Vuf z0b+!;$xZR|5~g`Asg!sYs3uf)Ad7mGv(>kk!u@dbxr}aZAKEhNKW`0!B%J>`Ic}e& zR1S+pA6;JGa-~Z8RP8alZsdQ4HEHS@;#Ke!1hltms(Rip8R}jtt3SXn5r-n!yZa*uRv_)j2w#G$qmsD7ifTgvsqX{(gFR`iVBhucV$F z6OWV_iSRo!DfuI9s`-czd0wLYk}Q5t&wx0SAR=!1uB~BZygb>H=uKCICds&X)f|5U zX#UE;QmB;ytIc8yH`2f| zl8b!E%Qq!fMWd~NfyFQ5$dR}g6?C10RQTsfi+~z(J7fYvBFKnd#)o-R<7~GY!1c2VW7>QE|2Y z%{|G*DGq?++Sc72oUHxyWP!co*x39x)a!7;e~X>oPA!Gk@-#Kjg$>_V_h0txw0mK6 zby;Z1AbNI(4E@FKUU3&q?nIv_@w21#e&2wyA4lROpe1J*in#yZ(&|BfnqCfl%Wp5; zEl&k^so;z8PK#8Xg2CbWe}NyC)GRjy_ldIFHV9S|x#idQd5(WDlK-GC;9|P%2CmYm zM$?l>*gUoP$1pz{V-TDIr8lsKhF$0}tr5l;i?;~t552p3a~gwkL`X)2%M7c^0M#_r zS7%7wb=nHd_rbVj@K)`g3;fUtjWtRKY3C;N4eRI{BO*5BHeJW%GuRz@Y{?h{nz93i>A08Tt z43R{^uRnHvMTH$)zdT7!4aH>0^_2&LK7ZkfmKT9nRkefz&MeLYh=ve}^ePVutJv5{El zWm($&`K7gUr@Bd#mto+I^BnbB@a3(_YoEzBpjMHA;zTNX#r2qfw2Vt`Boi$& ztDG}D)s7%5Vr7}NbE9p?xOnz7k|@DXQWgR#F=`)zQ?ScoAr>;4VnteQjOZ&N=a_(Z z<&kBw%QJfg+vLLkOkqed0Qlaiml%7eK+KrJ&_%W?u!XR7I2-wPY-Ve@l0C5ytA}%a z|MKGCcK?3rudNBDFe{;Y#9<}uh~zC%h#;rH};#nsm zzU=C)?Nyd6>Xt1QuxG^6ra_z2m}TD59h^w!L2AqNc5>M>Wd~B8l9v+xBvd{g=R+Uu zB`)+>_$a?*UVWp-6G`U(z_5P$Sv6rcL^s)c#)euNp0~6Gz4mWy2SYA@I(80w2$d`w z{yuqgyAGhQ8>Y-w0d|1*>})TAI7xr2$VXG19A(ZVu3_Grsh-P>)@yTCG3zOS>xXR( za<-+FCA&ka!I~H5DoF|73F4a?H^0jgT7Z8n+)xMmM~bgsFMRX7ugad?nSZTWsctze zIcBD=mkF$Ji8~wA>~1WtA-!sRIh<}>$BjD@duPBO?Y~1qZ1QuLzFH}Wcfqi#iWp78 z{X#S81H%gzm$t_L=;GthMM9{Q%mg?Hl@hUZGh^XN2hrpxS|M)r-3VhM*uKEeU3Rp! zEpk@3ax%YQxaFKi-*7{ty7yp0<3i!rLV>g1L;Kyph)EucRaxCGlfHN*n1i|W7w^HD zO3~auozB^oI^>1n<6iUtTlXO}!-s4E=K^|imK~;jQ{T3?N7T9FlSck6*CU>yo8vcH zg5Rz#=zrNO(^8a)!9df`5E|{dS-?sFZ)MEK<^rJqIp- zG0jq>Dt;!P|A|qQwz2siG}nP&n(F7)QnNxd`KawkG;dsI@&EI>Q6W+po9Md>BL!jv zAvAYxgs0})56R*~0*G?Obh`|1xNNPt^a}6? z)cz>gsOQ+d_;7B%-)hO01xc9`9D|h{B(@aR@>~EV;LaQ5o)6W~9jC$9JImXnmZweM zw67{f{Y%E$^A8W!7AcqNdic_L%7i6M`rNN38y2Hkh^F<`W0HJ^tFF_DB{M$%Hqz&m z9iyEcy&sMTN4l!y-JiFo;8eLv6zOi1f~_)yzGA#g{1J>l3CoC0G4vzJjUV~(_E#0J zpVe|3S9U+ynAt1+dgOUU?T-JaQz14=!pE`uYo1veA-VscDVD?ay*l+NHI-e`nbnDVP0@aukodZBRe( z@FXSX*dExK-xG4jk?X&5MemiE2Lx1BR?lXen%-SDRW_pPHsu+=Y%bT0kH=w+zH!)8 zK&B|uMVhc}b9;5{L8rWn&2L*(^~J+^z+;EVgd59!4(We6BtnY*&dnHtfQqPIMQuz} zX)~G#Rj$eO0l)oZgP{ti{MItg^O{6KnyjB!GybnJhLBL?lor9EFR`{SBgxkk)45dmFT{HZMj9yO z+Vi+Ow<<OYLYm=C<~~^eRN0zk&vzU z;QYm0#ly?_a@h6X-VAsgMETq#*e`5Vgb^s zA*8}KTil~76YQdy+GZ%HBr?cmk-6l0@`B*Od4G0Sn&=ekcDDpOM(?Wb6J_ZZ)FGHW zvaZE}SNFMcBx7`wk#0cG^GrS_-sI>uu`EurutD!XKIGZd ziC1tj{9*T>cnNx(ya+25R%jS<+@n4~@Dfw%eK^8}@^+;%^KGF4`|J8u^^&eA|cu;1{%3(r`V#AKX~v7wMouc5s&^A3$sL!PDqeQQl0EO3mY zHNLp5CdF`~QE8@1H6cGcLmC*^&dR{tvDkXohT?bMsG^T}8&F-{7WuKi%vRH!MC#Ob z#~Jr78i&+$6Z}~bA`E)zlVgz(FnQ6g270MMOWdORFXJPQQ>$g3Z^=HE&ke=TO9;Qo zFgiBngAN^QJM)6m@vjvkA4iU@LQZQgv@c)&?<9J1Aqye)1X0gLB#SX>I?>G*RdMGIa%;wJ03s2SG z?mw(Flut}~gPR&Zh``H0UWsgiJ}Xz}w@gRag9E(7%>$|jE>08@LmastM+L<_MXgT2 zR!8(y%g-P`o7>b=iF^DbW#q2e7ALXe1AQS6D|U_U0;9j9z03)&3RrpJjdHbZqQ*dT znZS(uuOyw3YQ9-Q%oBdk+D)_sFkJgiO!?H50FmdV?_Ug|WB&V%OX1zWV+S=iBuO zJl;P+$)H*vHVLcL7wvDPjv;dTsUK(f^P6Jdp-hzT9WEH%!zk6CWBfG{=rh4-cXKT^ zalx{fEh;LXi6XzaqK#G~693j77J2<4@EoffT$C`RFnun46x4C8kcYu zaPU6-eDNT4Zx6QyRKS;wngUvdZ#_SoUER0})$^H+Q(m17RXXYVwCa=?TF>+h#9 zcPK7saeabGW{N>jU~d<2Y0@X&)3CSQWHV8>|C6^JjN;kX{TFmhCG7L?S&~qGTHx>N zZhzi~;fVRS9R=CL?8>J1>_#Co&}avGR~6;u=4WJX2~1BH1?ne^e9Q=4_L1lym@I<8 zN^!rJ)fCxcu3%+Qevy*OKtJHg3ViQ0*_=AnIcIk5+bI-qC!}S$zdLgrkm==hP^Sij z3RwQeW!U|2>-*R1o7@b*KeKJIE@R|f9TvhcP#Cq#;iPWRpbZc-Zr4{S($Huc9Nb_0 zU6Qutm@w|8%N2HJf_#FD8yb$A2Au<)@MkDnv0wiPZSfBYdD%&AG%) zIqrH!z2=9#?oX%`pU%%u*_TAnC)e`a@8?!-8I(?t_~!zXpLNsQ%y_uo>maIc1SJPW^%T zfvOs_oh0sW&ny@EXP_1)&i!j+mmag=Z3I(07&U^4?1;KuG;99$bR=PVHU$1k^;(UT zQsV9dcT=DL8k5u@Q(X`LDskB^rMMs0K?P%tiHG;6i>(gdSW680yD_RIYoY7oU0hZv z)V|S;ASI9MzgkVH3`KrrUbeoaFSLKjL$y)KAIp|DQ8SO@DzzGl0H=W#^bsuu{6Dn~fi~iChVJlUVh1scm{{zyvdEfP zq~cc)*=qj{g!wH|`{S$(!GkY+ed^$#fKDALl3u`6&u6oZn6c|C{wjSiHB%L?!lLuO zgnZQ+--%13#wPUyB-c1Lmg>H+p3m^F53p6_fSp7mG0cidL2Tu0ra!<(QI22gtKQ}0 z$e&AYBoeH0K2md2PyJXoH36{!8%}`)ZGBijbaqSOEZ@Rf5zD^zYTrEtceh$2E0zQH z2}g3(lb_WuKKO0>5WZ)b%w0ii}VA9g)10 z2!2!hDnwHzeZ$dnS!gMrfJDo<1XUS`bsu#B%Tj4AhTPw_THRcK-I3aen^YR0kzU=jV69-AMaVqv7V0X34*$ z3x>Ab?oz_4;-`QpsWHNOA>$-HR9Ic~tv8IdKe`?R_J{=G@Z)=p6&muzD#xIhmNXyr zjo;Rve$uw$5?0Fw)}dUiJ0ghoJ>;MM5@0G;2RwagS)^o#U3RlrhBb4vB;=*d_~FRj z?ar+`I-o*$FGF2$tg#GCoxBBrLB=u2M?f7;WetvMRXJdjnD=nB>b{w84pB|dvGh=F zRRX)=_sx{!<-gz9f`uPY;QS?-WzA z8PIckDmUo6&9FA2E74W5z9IecgoFfCqckLk*} zabHcpH!rJ_P;psdE!#ToZqCE=v(&oOUjZ6>5R+-hAn0^WT>HjeB^Ok6wyb}70Rk7j zR||L1m(3Sy&{U86`H}PU1nNUu-Au||kRsk@eZU|VsfMybzrr1Lgs_9HNM!Nl{(48o zyl$->vo5kQfz^1$T!o{0%cpl*kxM3#{m=MyzUk4~OY5@-g)qw}E5-xeF(-RbCGnq} z!Q>K9WI`euK2icalwQf&_w0nP(NLsb#i=3`f>wr^k!AZdPtDhEnZ)Rh#Ig=R*8!D=%UcHnu#S=p^dsTF zzp+IY_OfG}<`pQ}#4e3a51W!fetSEgCyE_&!)GB%qQMb9g)EdxDm_~$KkAa+QiHc% zMZLO|;|#DGRdM9YpD(Drys(mU2-WEu0k_-O43J}AO4_M2<-c((l^4-eE2F9MEEMH( zbwig#E&h*4Ab?>!IhG!k%Ic2}2!yD=q+a#X351PpSv^A>Y2Fezfr>2mlXRD zK%ylvHRC;e+FLhot#qTYG_(FmYVY{gA+bvRL#!vDv*o@H1GRne?b$(iN80CI8a+Vd z3zEI zty~RfZKl@d^uLqCZpBP^fS?Mlu=h7Ug0~JZQ480iaisED*j_*bm9}rIude@HTeBCi z<;s`yazr&NIK^@EMBhmwe9$@H39SFl@Au)e=&v&O?Q;|l-^f>9p;jUTt6ooXjoUBf zyxR3$l-$*aa5~$#jhJ(RwmU^EfB6`H43@uS?0bu0sh@cw-Cbs1)}Bgj^$x(I5|U9( znMVW>#+p~-;nQc=zN7h%J%OM|XN2DoR$&BcMCa!w=0kjtew9Se!*;h;o{aO>E&nxR zJnI&H>D74I_RisGEj++~mg;z4llln$+7s&Md@+~OK{fX0^8f+tFdy5LU8L+Vx^$pm zs|6q!eAKznKWyY@xzSy52C-;ihc)Kb`!YSWDVK|7H>Z1X;sxmB1v2dBZKZl9rIcYO znUa4OWdoINud*J+T)efLf^e`M(0N?H=5d!WU=-WBfHih9M?Ld8IJ&M8t{fCC=%`IlW5BG&nBE8Py|x7Q36t@;B<=^qFT+qpA&hTm4=@A*PV zyX!uBKj_p|h*r=i^Wj9I@g0>JC5d!))fZHTk#rIfS9r-!&~_z24|VqGI8M??Q1_3Ax6&)a6fid~foFT_R3 za%q6kgl_ox?=hI^yhFsnkG$MjI{kB~0(HS#PoK)r%+69Kzl*t@?&L$@>y``e@S?)K zbCuLE%GXN1Alh+WE#fGLO#NSZk~+WyKgZo&%R3u+62{R8dV`i4(ya;>8;U6F5foq* z8!Rq6h7d*Onta6R{+=?NzyD5nJ7~_`r@?WM{Q3dg2lcajJg!QXKC0;lC4QwViU?Ef z0(MuTI&XTic~?pPl;Y%|$|iZN-(Q5Cw6nN=k1EX;493@^&BoOQK9w-&5$$&uJ%n8Q z>`~8SBzr0SrBdBcZNQjz9PO@|Jz# z^lU5U-PPVEM|K-D$q%<>d7Uq0;*;b^bOF^Qo@=QnL25MbnzKW=!H6srI^f*VQTkx) zvhgKkj<*QIk_`5>*&{ab@D7BtVsyJ##1V&VA~LF%JLz6#GB;5zb9LzH{mq-I8m$3W zhmmZR3Cp%@2bjk%^p`>XW#oW?6*)uz3WJ{8-{~4O7B_8#j$2%#)AdP_Rf{;PNgWt!+$-jAlA%AJ?c)EMI z=CSyJk>0KnKUH}@@;OY`x}huh5}r;q59*-ue^?eeyO2!khL&JKU_DLAt3Un#qZ`uO8YaO4sv(Z(AHPM_Xyg8LDzs5-lg7w@ z%4}-tC)puSD8rfYNxucr1*D&D`;?~lFqjzO*?(1G&>f@W9D&ST z-oii*L(~wfgtANsAhE8|zo8;bZ6%lQ~u&Bjm^GlZBBG! zl1+5m40-#iXt{x_(2Fz0p;Z7^c1}F<=U$U4-ssl>UYjR7hB^zzf*1{IJd2Lu8mBM`KrdZpuT}e8NR1L{==;e$ z2EI7~r%dfn%HM?)uzHx{QV+Gh<%V#|XYYvr6TJC$@N_|>0X$VS;#&G2?rMZ#JQVqT z_4^QdjF1qsSKnnhEp_*MCsO`x+0|AQe?9bWCivr5Axz%(-7i+H0Os4)d8E4Eu{=5M zl_1PC|7l^XEOxf}^kfkZAJgDEZ>bP}E02F`vfC`|i9=mCu82?xNN{vTk&qd$y_p0cu--pKUWj~{S zGiz=BqG3nvRW+=3)<;DVCUnI)%8xnsROQs*$3dNmcoXn43=Kie9%olLVo)oF(qVme>esVbaZn(KWgTdESrPr=8uu-#OR0 z&ULQ)!Jnqg>3ujv%MBh6QGK%0$8bdpbvlW2Qd2MXIHfw`9-Hv6qwJK;@ZcSLLr2;pc-%T4QKd3dMHkqeQip%B4jCWu{h-^)HJRLK0j9u9Oz$HU@u z=y$2F)w2#MD{YWj;GH+Gng{DVTSE0PZjH<+4gYikm zSFyZSlFm+fs!zH8yE;lPXh>`Pz`T;aJ=$t&En~OfYF$@cmb%%_8OENIKC1jqn1Djo zko*>-0Ar~ehL=ZB1wRxU$Z@@V1UR37btZ6u@z%@Ot#n;QD)*mO=?Na)-{~^4CnOt+ z;m63y2qXw*qu)8&aMs^3+<3VqU$U$h#dunI=yCxPuvHeAs(bo`JS1pw=TNt#7=eGb z&-#$JlgcmP2_ecfV2&;>r*cYhrDRy^iWw{vd2y7OeRrwhWMj5;RaDL(i>LM7!{zXj zYo`V@w6oRt?6TsC9f>57^#T(Ww?VuuYoK|>-q0WMD6@HIV(CquCI%^O#RJ`|;a+i; zjCZVp@_J})Hld#*moJ~Dublmvaj_BxEN|^|BC$ffvh;)L@ERO9WQ+6X(CL0gI6ccpcn`W9zlB8ZnyoB?B=C#+f%ai)@YjagoAccwXxo#;5X9B6@Gx2P`LvF((7=(+~ zgRWa5&{*uCjMt9}BY3jfsV8`pTT)V=Bi~bLhnf_sLY)PzDeC+hq3?61(m7uFCC_#6bdj14J8)|W(SaeoiyX-ukmXG(=Ur_1!>B=ydShQ8{7>!vd0{WI0s_3%Vq1 zTo^w``!1_S|KXFo(A+t+>9zjQd^(jJ>0d}CHsi@6kU>WkMKvM=YHtd+A2Tp zA}Jkbwki>m5DZW3;!js8%DFq5FC7;5>t7QIX>=n^Y_}u`RsblyI)O(b{_pBcyMLk_@KVb zqw~`^>Y)Q0=MW(GvcAt65{pESf{RvQAXYK+}WZ;Kn_; zQX%DI>q%PC41{=Gk=}7>79+IW_Y3Lgxl*Iz9Ok~{^n^b$ys9fYt#q;+^)ZG7k3+~P zqf8)D|0k*^xLK4SeF|8{@ceb;2PSaR8o6P+_T%0yAzBrc*7J5+2TTI1AwTJQdUh^t zSM5g^S5D3`67wR1E0EdAgHhi#%k@|RhwToO5Vh9&3-a&+(XU*?M}e4y`pNPE z(7UJp?!6TDnsk(=u(yR=`5X_K2U&$^V%@(pj>{MN-PN9&ErVuNeU-6v)gU+wM&+_e z+QjUFmLJ_+>s*UgLW-v_+bE(wIB*}J`sF*GL98ESjdn=S6>BR*)Y+HJX;ZU1sxsD# z{jErhmy;xec$ia0=@0ntye}`Z924zwv#)sn{+^u z5;1AG^n%Y0+ltC3SySny72RKDC5T3%xvG}k2GFD$3c(Epse6JzG@{1y1+NW!jJ=Ta z2t_CT_2aLwc>Y;X_E}II|MODPzIXM<@h(n+?!}jL2V0Ks(uQVzHHvbkc0Y~HM0Ctp z!9{1-;H-WdQ%}{(-SB+ll*#`%%4K~L{Cz=JyobO4B@I4;P}=g9!fnmdtKm_W>fP;+ zt1jO5RQhW29Tx#>cuT&!C#-e#ej0KPy_b6MWa>107scIHCu%lTdp??3^8qT^m4<^D z$Zv8MIWZ5eBT@W#$0UfT6WNA!Tace!|D#HPUIU;?Zpd)}iUHU=nMI2-90$;em^)c8 z@s}~_74H_>Gy5`B1PZl*HX{PPqLI7P{qWKU2y^|+Dunbx>Hc}7s5|F%Jj;1gn3O*k z#d*MF&o`JZve&1noUQaCzthWGTUv!U^re`d2Zwom5%)6fOjl zqZr2|8m3~W{72iqgGaog9I&fy~jy zs{+e{?&M(~(>8qmM+pVUM}Q$Ez@5n8Mqpb4t6xAu?*JCcW$Yj0#U83Uo2oPwkvwir z)G*YREbj$zRW(3!z#xc=es@qr%>0yVXH`7Rsz428Eu=3o;)AXkpR{ndS^pFN(!5~_ zx}dtYWd6WzuPLx~h10JgChRn>>e4N{*^W>vT>zZH&%Kk}`XPc&QVT|v6bt4oeO+Ph zv{N0N)nX1=QcH~Zf@erSqC5Hi?{GFBOQpbGdDQQ0Mxm%@Y7&)4>iCk;`KL_XDdL?E zRz`n)-?%GZFue4ug)5F=R($oXRorx@ZOBNvg}7T)ex`s@JL)6uu0e^$KTU=CEf%}M z+&694jh)9sfEG-^kt4NdqL;43o0U4 z1S-Qq>+T(W8YnJwas4ykg8p;Rn$6)EY~XlYn@44&+c$vousrg%Gcb#JMWYIB*Y4ixNSF{f^H7`1FqYMt!@}H(MO;1NFK?k`J36Fw6rd8Gs>FC z=D!OUXuF(UI}I8nQwtiWqm!LOUTnsjfQS5dFD7O@53exQE*qdQ?+BEBs=C+pmM0!^y08@BhuQ?YQzGPJLu-?D4y+4Xw84Qn-gs@ z;)nHFxLVOx|L}caYk*yNvvwn<`*r``c&Hd6B!;Q>TGt{v*(LEIYM z7CZBSFH-q=dW0e~6C$&QqA%pZ<>|V`pM4vPj&MtdJ45SWHV&bvc{&>uYe)_{E^N)) zbrtOSGd|^FQswNalU(lB)tI^K*PxI9S@B`%uL*}QLbZ;4o!TWFGzZIlub8{)?IFmv zlW#Ihhm~xV`{SflShL^2hl#)9ma1;h$`c@~;oK0w9RGSN4RR&Ckl^Q0CZ`qAWfP19 zb-HQxO{S)DjkX{wr&MQw9d7vW{K?U2AQ!4fLkB~u9-%Gn`?H~%->ZKez7=pfzx4B3 z4_fx-cUOVTPY&v|&K*Io7CSguxB3Y8>YML&f{0joixkM8{4KR!WlsLrT?F00P$0~kSs$=c zZ%fVxxPobX`6WZl-ciT*)k4=@d0w%sZtRix&>ggTI*}P>>l88Wv_qGu21QW)^cnE2 z#rq3}Z$G>ebSvNd+_wZZZl^q z)NJ4o-Fe3i-a#+hAJ#c5<1~v+p0FVFNauk+!C;fj$(E^!Wunww zEcK8hN$PX_bQ8la%OJV^MlAl3%>d1}cG(?u{`W zKkg^f6J}nin!4iev}hz`zNV@-E>foZ$M{QuH2`%-q!BV!3GY5#k>G}7~+(azw zI;f_J)zUOpwlt*#2ZlmMKh=Aa1$`J$%jLC+WNTLIrPJPi5cJ{N?1Z;h?R2!|{_cYJ z!+CZhucpn|qNevwxbN;CyZcZO?XZoaRsvsKd5S7yyLR88C!t8!#?E4aMmMZZFi>DnR3ZAz%+Mmkt8+j_IzrPH$CoP zO8k+m5Cs3MmHTZlT#G zp9pS=7D;$s?tJ?GQ0co-*i#Nel0Ns-O2R)yyFVfcQ;j+p#Qs@!(9i)b(Blk1GY#;P zKz>nR#DAc1emLYhRAb1F_gR-Aah1_@vGUID{PH}k=3%qL&iV9K!8V^?S#2A0dDJAA z5znC)#EWQVE2*Tv3KiM%`BDNo&P03$`(y2B`f+460O1MaXb%dUL0z6~PfzhOpUTDh zHP;>GB0^0?S$`+={qoDgYl-P&(Gh)8=A=x~@b)tMuKvm2xny5-SF@Tv_EN0%O3d#7 z`~-{H@A_mo1H^05aWoK*i{Zak9>oOnpu0{#9<3hmqaw}K2DQ`}cw*~;slRCg-;2B;bLT|5h{zop#jmAZag)sZ48P1(qQ{2hQ6uyDhBj2jC-h~_ zicZCaO=%q*xd$xADMLoSEVF{g4F!Gy8-tBpO z0zA40WRF>HeS?6?uK@u3ked;xv?X4waf(r425%25NuoYUne15M$^GeTis4Hei!sux zMTi%zICR#N>j@a7KG7_pw9tu+_wf-@#SA)A*d8hdg`$1!l?CZN)t+3plr_&XJ?C^U zN&fuk&FFfj$d(lX8h}R^p?p@pV%x|1lJ94IwtVRHoxriA9Qt)U_nwU}i4Ko`|5~1R zH1qwG`WT{(OkHqHhtEAEkT&?&!Ba4?_ zmDzw@Je(2Zvtp_<3@XQCIYmq9~UcjN4_D*U)fF9Wzc(M^79 zenvkyrQAwS`ew#9!PTs!{Kf4$$uG6^gWc@vM@9oVQn-yrnU$3ETfdaj1~in}_TYFl z9kvil<5^lhx)(C6L{4%$-~`eMa6$o|R0}sXj3-w?Is$5Aay&OKUHvku(8NKwSG3GuPo1B5-r!{&t$iG(Aj~qj$oJ&l{GKmL0CSN-` zuzY6p(~B+%=5=+2M^?A}y}5SOz_kNdGfqGNdQV#FOE{4S#7l3!Osh&)@daSKmYCy6 z+oNW1jkz*)vjyAeM+9|LHJG_uZ>)vC)Q0WM_Sgg*Y}EYMXV~mK4xzF}rLC4!7N&Vx0+F z;ZI8v9Z1w)3jYNR>O98~P0_6HRUQ$L$yxA?m95pxqlOzd%Bz+((|kH!=Ug9&3_f~X z*sI>dxwYCktJm*TV=9=AN@w&yeW z!d^#jFFa%-yWKx3d^ud0|BZtqGWmZV7vkn|J?0U_56=fOM=}E`KAz4#Or*y?!PL8@ zJc&(VWRp;LAvaLfYkJC&1iXKNzN!uoT^)e1p6sBeXxMX{nz$0p@3^4LSysTEPu_Z= z&C}A>z=>N9`In9`u+xUu9h9GVwn0(db@taF?ZWHoWzcmqyu;ji)=qLX2b{8Y5Ol3N zN|5B?{jB<)*?z|;aLV)( zG2DD|A(PJ`x}s|GV-qG?f^{&U_6m-V;P5?En?dz-+euVMdd~_pQ8{L;^!2*uG-~i|($FBc`nTd_EB)wehy}6a2b}@z zB57Uw+Oz~VBy(+hOVwFu9`oA33yi;z-%D=Q*<~U~{;rN6<3~#36#_LDVhAeqYr+-%Fw`Z-4_co z6)lYe_Jv9#R39RwuAh>eXdO~rz<%wYvn!u28?RL%hVaMd#0mRPw-IZLwdguiF7KU` zAdsY|uBYhaVsG|q6$u~_c zqJD~p7dJh7RB8EpA_Q|d0{V0z8|=t%ZaZlcus|1(=XtGfX7mkKUnWrIb=iuBsb7rS z)Nltl9bdiTF?(*r-ft)FyA;JS5)mHI;MjH0z?rp~0vdK$nXjj0USxVG;Qbq%&D)Ea zrNFN&C6#+0`uPX&a;Gln1Wbp=vucEmSI&>IJ_z7mG_ z5|_OuLP1jezQ9==$Uj+B?bx$NP)zOJ;bCCmKxo#rECZpm4Fk@mkJ*5m2F*`!Q z#4RCFSJytjB1wsY1?Gr*x(i;MFIn7gG8?4!KJ~ELJF#i=@H9=`_@0tR^laoa;!Vc+ z{OsnL@+ho5Fo0ebJtpb0_`1Z7;^~0d*=f(aveh+D=y;9SQoN`e4XTIzdbgfcf z4oIEz%){>%_a`iOd2PRYEXZ2SNxA6}-)pR7&E%9}G)KmCTfTtE0FLnS&AL^`=qnZt zC6Y&B9nAi#DG^kBy3)U66*cj``AHq8U0@`kRq8fgqabUj{+V$BUgt2Uxf$)OXt)@> zR`Nq2CO+-($&&d=G{5oqbmzmXbnO4cebe0sw0w4x@I+77m_7NlF^VApO^!(vS%mBR zekjA$p&a!q)TZ$Vj}FQ6 z)@#%azqfRTFoy`8p7Ygm7cvz`zA8RSR(@ht&umPcpXvuac-9iehkeT<+$bOMZl;t^ zaO3Y-O93x0po|P*_H8mLP17n>)Yo<6(n+E&gnTq@Th+x}p=&QKMm$_j>5l`eHr@p_ zpgY-E>$7+N7Ecy5*m7mNTO?eSj5Ci``(L23rT3a?z|w;VpCcus{)?VZ2>aPpy12?? zeICu&*(G}dv*3G=jUdy7OoWH54C(88L4CM8m&qP*Wk!+{#{8}*#gXZsvB#VqZ7Gs) z(t_NjK82!^)<+g8RUdYi>V%*4zkhZ~QY9jCtm<>v#Fx4DqXT&=%*kuXqVnn%1ydt# z9vQ%Y#o&Glc^(PSUSfw8uJwxgQ7O;08Wr_5CS(q9x8RFRa~EVKXAW=}m?+%a%a>HI z`Tf=j{?*!OD^K~*f=dNV#Q(_C_#i31BVb@s*VyZN8vPf%6NBdfvDWWB@*C^Yi`mgz zG@D(+b5V7p2s}M#Fn;x+i`|c4|3e&j9(DH$thIoKp&bPfpm}Z3UYf;z_NP$GZ>^u@ zXbhnCYn!PT5YFlvd2R}?uQ9u9TfGD$_?^PL@`3hwqF1x*`Wbh*92XgJIOHt?HnthdFB22gc2|PCDGC3;Z4@QSa2lDydqqF!sZzX6h zrxz7Wbd=G;GWyjaWV6Ie8RWT^A}^=uk!!~NC#d%I5Jo6NumEvA+HOe^H!-r-&O7+W4 z7it&5ue7nJKmNOG;9G854LFg&CRnaPB;TXRkQdKB)Ag8iF%(x9E3#8pu_>jcX};VH zu64R<73(YTdKW*dXCCf;2@3|FKb?Z3ntvQAm=)%}qn-Em)=Tp)(S3Ps}c5N88YloH=`+M-r8YW5OOER;@^ zmwXm}c~6cnhubIGl2Qy*cfsD3)cw`4$}44U`%UycyEkmC;uB$ajkz79i)zh3s=ZjA%JDX@^RWG|iW{EhjnDD$hi zyj+Qkz5njHLy!8-@6aXqqP&^8dXdEV#$Hd4lY}2gA5-ahhyYiA)+ksq@I2f4Q=-Vw z{~dWa^CTK9ff8 z9iKCwOP$AmHW7YDLUEvi9{dZHJTemY|Dri3ue_G<{Uz}tPHlQABh`J2pK#6MQj5a` z53u)V+bX~n`nlXOnyFHHMG<@8mcP1z_(0$CgHQNe*J=yVGX7th)nhBZUT5bMced7Dq0C!aEG4F`Q-Q#RT)KVFr!PHs)sart6NOm^s;f-t%KwM@uj`HBh=mJlEOTCvq}t)P zof@7|hHIzguc0wKFUqgHKi<^+tEaDXQAMc3SLN)w$!J`p9Ttyu4Z6r4t zT3cCt>jW9uhdb*HgOB_%9RDS>#@&8}DCYgBkW}hn<7KC4h99pz+7!2L{4~S0xwC|g z#e64hQm7kapr=lnoK-3VvtTn5al7MX)#i*0!7-&x_EE8k;4m1PzkZ#WVC?zB$izdmv&;{SRww0XM=o5GqHJ ztH0gUa@^cIJVPKGhG%17#0WFDRWVqE{kNoP->f-Y*l&BS(mjBxHEDz#vDC@ zh`CRS$8qDod+$sgQI=93(^JjlFVH1z8RZ3tH)k?*FU4nSy>!mzRG`>N9GIOt`%!tF zGCw(-t)5&tOVID(qjuyty1Y_;}K< zNGWBW5fiQu>uc%E6@5!7rBBn;+&8(OR-=igvpL5st`u}j_N^sda89g1I%8~1TFS}> zMRT|WEm5mIo6Vz=@uEb?H)3Z_X(c7Dn$ixif>C47>8D5ZU72Ffg@`<2{TCbRqcY#P z6kb>Tq^u-WV|}ANm?d}`$6hlVELM{$WA}ycw zTGHcXA1pNfmR0&CD{ZR$EHOR`|7<5~p!}?&H7KU#aCmR4X4Pq9Iy^f8jxJ`ul$Y)p z6u&MPSi?*g_#M3pvS8ghc;7(uR(YXrIJt>jJ3WlWR=EtdAnNVg?mmQR1nWP_t{~jQ z7eoE7@--`3`f&Htkye2YD(u7SJ(Sb$AFco=u{ zx&Q!sw50fr+ZZ!ianl=_(lZnw->SN7UcDVqw@&o)JNB@il>vJk&2m-2f7&gKuS`C( zH6DG=YgAffpS9zhFP|*;lT+~wN*g7UUL1_g#5)R*HzQ@o`zMz0z#PI zockAQdUe;;%T@iI8|@3NHf;(n(t~$t!atoR9AaGtjN>l)vTdD+O(RO;d9=v$0P0bD5h49tK&yi0SC1dF8& zi-Q0^<}_cGlea3#1G`kVDl^o4ik?sW-fX68{k5iOH>;$--m0P=+Dhi9jzA2Ri1Uw~ zw%|4_=~HP&87(5Rh8H_u{;<5q!i(9syna~+yI=XIqD%_wuJ*t_^fV=i!Zr0778X`u zR>9$m*9be;7EUg`n=kHOURHQ77VMSi;$0iLHRu1~zAb)Y~ua20~}o|2`#G zhN!aQ-xgdd;HOgqKi!%|=QH0~;VX_eq2iN?%nh4)RUu3LJ0atff#RGacB;fhAbhf6 zTHVEG9Hx7T<@|(;FB?T-Gf-;O&f12=DRi9`98If98~&gD%FW)mrDKI3efZ2&QKshG zCMPv=N|?Hsr-AQ8N~4PV3#UzZA2DodwDmz#jBAs=QW|25d^0PNyw(~vS@llUlyCCu zKIHzXr&>|@k9J+Pt9x%4^D?o7*AL%kdvr1+7bpqlO7u9;(N1+gTysy{G0^;Kp!n}l zfdRqFI4}l#fEyL^E;n)ss9>dK__}O`DXog+mINM|mr$=~{or z^n`%xr;B@I5`S(Y4Ff<1-uLx7X_YL@-Y~Y_EkG16b-W$){p%hIIi0AlWT2n%#`#23 zcRtM)NrF$-=KkI5g~-C>{5Xy-S;uB~hBuoWZ%9HhiM1tG5D`Vxv;X5b@yNb#;t}F@ zbV)z@#X_vyl)jbi65CfhkX$s=r^Qe(ICY_FKH0Fi8bE1}8V)_1uQsDt=T4ct43KP1 zc0)o8tUE{dOrNTFo*y*GoM>2rfLEhT*&RVu-0?4!tw~#t^7b3VFR^za&Y4z8A{Wga z7Zo+$<6&WUnY!s(X1At-l{Q$sRUtVMlc?QAADknF#c4@WS&CF<_udN6AR>1c?glcZ z{!;tE2G!(P(KC)ZD4v{8wCqC7(5P%XidEHN>q%utq~bR+L*Y9u|5_=!8vq0lg7g4z zwa88X2H0SKqJI`eS6u3MxKO>tNUll0oA*q7j^d&SQ$1Iu2O4^+zIi-+ea~4qZXtsC z^Mi#}5cSp3tbFtO>UmP#Md1oqQG$^35r%v6TAvUNo<5RPZd0%EDu9c4b!j!&BCZZMZ^l>B`c%u#1Pj+DX?amnS2IQY|t*@b$XYZ%qI@7)id%aDvVPC6AN6)l>2 z@tb5t!JP}HHf8sGN*~qXT2|D+l5TiKl_SLb*quTjRJRJ~g!+?0^Z`GFt_HBCLIT1x@C> ztrg&rX%u!loojlZV*;ktZ5W%{=eCx>c4t^}LBCv@1|rL_n=Ky}8S}R${(4_*{|NTB zseoV3FoU*DzO%L2x?A37#Yfopgh-)KllnEUMPnbH8JpgzY_-1JBePOgdoAKQMepwM zJ4(0lJv9et3&6KfkHsXPFFDrKSh`1ol4rqR!b;MoEX%D2J@j;Sgdk>6B9VeT5+-N8 zPd(8Tu@a1?i=dY+*=Q9M;L0!S4}{cJ4K!G)W!JxLNY-YtR_QRE>#J+3bUOK7S<#6{bje-M7k--uk*kP4C$`CVT4$k@i= zO4si*d+YDU7R>IE7I(7tMmYN}fjUNxbF1G@t{W9UOk`+xbL-A zVW6fORiE*4;=#vPxlCk*skM%+YI3CLCNVB+s<(cENRFuo&iucJY7yqwHImQ;XkX@* zQM%F#_R_z>IZuPQWsEQOJ+v7W)52R7vXkYznu)3p-aA}_bp@vLCGmuaX zYTE8fw@YC${KFYEU;sgl0V*#JfDW(_zH#QmzXvqVUzmtl!=z71>b`!MjD}hkS{IG? zC*b;g_njU6_7*XX{Dhd?@`Pgcz|erFGv`C@L5ycEaX~atc7g_vzkWlZDv>=3v%cT@ z{R`21{Xb_&W6la#Y8Q$Z^3}bx@Nv6wY__kO%tcA{$YUAi58%U>+zEi+eHruxi|9ig zJ8=`~ua`)zv>>oMt*#JZ;R3G@>uYt6W3MPZHxCKzqCUQ|HIUPP1OjPSpo27)Jp0L3 zquejG9H~c3{_KyQO0mEF2Y7E{0~BmbY`0nvz{?9zZpx6ge6Z6hGBwj;WaEB?*Aj1c z=R$nJNd@s3D$#s?yxs|)k2qdXAE)7=p@F(D z)^M8SUc=8pJpF~2s`eXOFk{4@Pd-MN0=U&uA-Cu;|dhXw{*_Hps8 zDp+>q#i)6vfJlYEKRk{6H-rEJ1LTJ9ndpI?Z=k2#BM0945ezut(!a+KKdG2lEjKPN zQ!1b6n$e96oy|3af4HvuGwKN59YAu-3QixDl?*SALfAmd??v=yI7${mpF+hNa_CSa zBm0waq?pnqE-}ZPbAq@mCy3O{;b^QX#w|%KGVnbebw?2j+llB!A{w| z(Toa6aTIOXo<|cbX+-C_ZIZzY{Zs9|z9o%5!jTnW+({^YQpo!8s_>3H8Cf<}c#}q9 zU7oi>$;&4kf2V+V-uKRX;9p;y`4ak(cS#EbRKF#d~6G89*qhz0?IHRy^BGkjSsGAY`RM0 zRrv=DB@?#HdlA>~NBytnM=lAEGHt;7S18}pv2aWcjQk;5_4(`tcPVdbKM{Q0RP=q- znmg}AN@)Ffj=bUtnck<=IOX3Vu3BPVCi=92Y(^BO6x-Wofr3I|JK7ZVNwrl1Y8o&6 zlEM!T>q1B=6G^@sXt5V^8)6SfeSC(U2dMFY|Bwp!4@qtWT;jNi!Irtbmi5+YCcrL| zmyFhqWfxM@%Qd}1%1pd3dL$A#*37$yyxscY{03Ym>WKL%vZVr30LOcb; zBbpyXJtIRXX5Fqo-B;Xq>EC)9&UhBl;r%j-!pupHP%yk0G*WUn?yi~qTlT?XzUj?8 zpHWrC&sUa0Eo=mf;hG9~2LHAFL$22Vxu8p_K(2!u6AZipgzYggj?AmaB44*$CwcY5 zO_@yXiexHv<s|)tC6$^kBcp2*l*eF^P2jaT$nUi} zD|;n{ujosNxO5QQmj8JJ2gzUI8C!@%e_fuC+zCY zlo+26NDT8Uwv|o)rxKA(duaY;d`WPBo0rnQOr?~RaI)-iu+@KmUwrgTJ^4>(jgmse zQ=x-fYeMLr9+{7=O=;itz1W~rmMSU(EG)_gFRu5uB3}H)93reeDhoanCy-!au_gOL z<>BWh_i`j8|BFihokvX1fZxP&x9Z%(=kky*H+2AL5NT#LKqclubPdIHl5>{THcCS= zZMWf)1FXYk3g&GFR|*q7GWc3-;_Ag5CVmbb;a6FBKWPtahT7k}P?s2pxgT@rD@hT$ zd|h;8_ff*8+gCTxZvHZew(NfRL7f7{BqgSg*6x#8>3 z-F7!6FO`n@DY?;43lL59l2bQdFBUb~F8}C2C1>-2!Y-3aKW?gN1ef{{cyNc0K?A From 64aa634f9bb8a64b95b32888a05e59f17cd36c31 Mon Sep 17 00:00:00 2001 From: krocard Date: Thu, 24 Sep 2020 10:19:11 +0100 Subject: [PATCH 059/110] Fix offload buffer full detection after setEndOfStream This issue has been observed on a test app stress testing setEndOfStream. The issue has not been observed on ExoPlayer, probably due to timing differences, but it is fixed preventively. #exo-offload PiperOrigin-RevId: 333472136 --- .../exoplayer2/audio/DefaultAudioSink.java | 30 ++++++++++++++----- 1 file changed, 23 insertions(+), 7 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java index 41e76440c1..478eb0d04b 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java @@ -336,6 +336,7 @@ public final class DefaultAudioSink implements AudioSink { private boolean tunneling; private long lastFeedElapsedRealtimeMs; private boolean offloadDisabledUntilNextConfiguration; + private boolean isWaitingForOffloadEndOfStreamHandled; /** * Creates a new default audio sink. @@ -712,6 +713,7 @@ public final class DefaultAudioSink implements AudioSink { audioTrack.setOffloadEndOfStream(); audioTrack.setOffloadDelayPadding( configuration.inputFormat.encoderDelay, configuration.inputFormat.encoderPadding); + isWaitingForOffloadEndOfStreamHandled = true; } } // Re-apply playback parameters. @@ -932,13 +934,26 @@ public final class DefaultAudioSink implements AudioSink { throw new WriteException(bytesWritten); } - if (playing - && listener != null - && bytesWritten < bytesRemaining - && isOffloadedPlayback(audioTrack)) { - long pendingDurationMs = - audioTrackPositionTracker.getPendingBufferDurationMs(writtenEncodedFrames); - listener.onOffloadBufferFull(pendingDurationMs); + if (isOffloadedPlayback(audioTrack)) { + // After calling AudioTrack.setOffloadEndOfStream, the AudioTrack internally stops and + // restarts during which AudioTrack.write will return 0. This situation must be detected to + // prevent reporting the buffer as full even though it is not which could lead ExoPlayer to + // sleep forever waiting for a onDataRequest that will never come. + if (writtenEncodedFrames > 0) { + isWaitingForOffloadEndOfStreamHandled = false; + } + + // Consider the offload buffer as full if the AudioTrack is playing and AudioTrack.write could + // not write all the data provided to it. This relies on the assumption that AudioTrack.write + // always writes as much as possible. + if (playing + && listener != null + && bytesWritten < bytesRemaining + && !isWaitingForOffloadEndOfStreamHandled) { + long pendingDurationMs = + audioTrackPositionTracker.getPendingBufferDurationMs(writtenEncodedFrames); + listener.onOffloadBufferFull(pendingDurationMs); + } } if (configuration.outputMode == OUTPUT_MODE_PCM) { @@ -1221,6 +1236,7 @@ public final class DefaultAudioSink implements AudioSink { submittedEncodedFrames = 0; writtenPcmBytes = 0; writtenEncodedFrames = 0; + isWaitingForOffloadEndOfStreamHandled = false; framesPerEncodedSample = 0; mediaPositionParameters = new MediaPositionParameters( From 30be792a15d16d32bd0c2bc82e64de8e74199acb Mon Sep 17 00:00:00 2001 From: tonihei Date: Thu, 24 Sep 2020 11:28:24 +0100 Subject: [PATCH 060/110] Switch SntpClient to time.android.com and allow to set host. PiperOrigin-RevId: 333480727 --- .../android/exoplayer2/util/SntpClient.java | 45 +++++++++++++++++-- 1 file changed, 41 insertions(+), 4 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/SntpClient.java b/library/core/src/main/java/com/google/android/exoplayer2/util/SntpClient.java index 19159ede6e..03336fdeba 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/SntpClient.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/SntpClient.java @@ -27,6 +27,7 @@ import java.net.DatagramPacket; import java.net.DatagramSocket; import java.net.InetAddress; import java.util.Arrays; +import java.util.ConcurrentModificationException; /** * Static utility to retrieve the device time offset using SNTP. @@ -37,6 +38,9 @@ import java.util.Arrays; */ public final class SntpClient { + /** The default NTP host address used to retrieve {@link #getElapsedRealtimeOffsetMs()}. */ + public static final String DEFAULT_NTP_HOST = "time.android.com"; + /** Callback for calls to {@link #initialize(Loader, InitializationCallback)}. */ public interface InitializationCallback { @@ -51,7 +55,6 @@ public final class SntpClient { void onInitializationFailed(IOException error); } - private static final String NTP_HOST = "pool.ntp.org"; private static final int TIMEOUT_MS = 10_000; private static final int ORIGINATE_TIME_OFFSET = 24; @@ -80,8 +83,37 @@ public final class SntpClient { @GuardedBy("valueLock") private static long elapsedRealtimeOffsetMs; + @GuardedBy("valueLock") + private static String ntpHost = DEFAULT_NTP_HOST; + private SntpClient() {} + /** Returns the NTP host address used to retrieve {@link #getElapsedRealtimeOffsetMs()}. */ + public static String getNtpHost() { + synchronized (valueLock) { + return ntpHost; + } + } + + /** + * Sets the NTP host address used to retrieve {@link #getElapsedRealtimeOffsetMs()}. + * + *

    The default is {@link #DEFAULT_NTP_HOST}. + * + *

    If the new host address is different from the previous one, the NTP client will be {@link + * #isInitialized()} uninitialized} again. + * + * @param ntpHost The NTP host address. + */ + public static void setNtpHost(String ntpHost) { + synchronized (valueLock) { + if (!SntpClient.ntpHost.equals(ntpHost)) { + SntpClient.ntpHost = ntpHost; + isInitialized = false; + } + } + } + /** * Returns whether the device time offset has already been loaded. * @@ -129,7 +161,7 @@ public final class SntpClient { } private static long loadNtpTimeOffsetMs() throws IOException { - InetAddress address = InetAddress.getByName(NTP_HOST); + InetAddress address = InetAddress.getByName(getNtpHost()); try (DatagramSocket socket = new DatagramSocket()) { socket.setSoTimeout(TIMEOUT_MS); byte[] buffer = new byte[NTP_PACKET_SIZE]; @@ -282,9 +314,14 @@ public final class SntpClient { @Override public void onLoadCompleted(Loadable loadable, long elapsedRealtimeMs, long loadDurationMs) { - Assertions.checkState(SntpClient.isInitialized()); if (callback != null) { - callback.onInitialized(); + if (!SntpClient.isInitialized()) { + // This may happen in the unlikely edge case of someone calling setNtpHost between the end + // of the load method and this callback. + callback.onInitializationFailed(new IOException(new ConcurrentModificationException())); + } else { + callback.onInitialized(); + } } } From 31251a40c1c504c78874a501a7e5e872ae68485b Mon Sep 17 00:00:00 2001 From: kim-vde Date: Fri, 25 Sep 2020 16:31:49 +0100 Subject: [PATCH 061/110] Merge pull request #7968 from DolbyLaboratories:dev-v2-channelConfiguration PiperOrigin-RevId: 333485323 --- .../dash/manifest/DashManifestParser.java | 48 ++++++++++++++++--- 1 file changed, 41 insertions(+), 7 deletions(-) diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParser.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParser.java index e9e9c66df2..8015c139b4 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParser.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/manifest/DashManifestParser.java @@ -70,6 +70,16 @@ public class DashManifestParser extends DefaultHandler private static final Pattern CEA_708_ACCESSIBILITY_PATTERN = Pattern.compile("([1-9]|[1-5][0-9]|6[0-3])=.*"); + /** + * Maps the value attribute of an AudioElementConfiguration with schemeIdUri + * "urn:mpeg:mpegB:cicp:ChannelConfiguration", as defined by ISO 23001-8 clause 8.1, to a channel + * count. + */ + private static final int[] MPEG_CHANNEL_CONFIGURATION_MAPPING = + new int[] { + Format.NO_VALUE, 1, 2, 3, 4, 5, 6, 8, 2, 3, 4, 7, 8, 24, 8, 12, 10, 12, 14, 12, 14 + }; + private final XmlPullParserFactory xmlParserFactory; public DashManifestParser() { @@ -1156,13 +1166,22 @@ public class DashManifestParser extends DefaultHandler protected int parseAudioChannelConfiguration(XmlPullParser xpp) throws XmlPullParserException, IOException { String schemeIdUri = parseString(xpp, "schemeIdUri", null); - int audioChannels = - "urn:mpeg:dash:23003:3:audio_channel_configuration:2011".equals(schemeIdUri) - ? parseInt(xpp, "value", Format.NO_VALUE) - : ("tag:dolby.com,2014:dash:audio_channel_configuration:2011".equals(schemeIdUri) - || "urn:dolby:dash:audio_channel_configuration:2011".equals(schemeIdUri) - ? parseDolbyChannelConfiguration(xpp) - : Format.NO_VALUE); + int audioChannels; + switch (schemeIdUri) { + case "urn:mpeg:dash:23003:3:audio_channel_configuration:2011": + audioChannels = parseInt(xpp, "value", Format.NO_VALUE); + break; + case "urn:mpeg:mpegB:cicp:ChannelConfiguration": + audioChannels = parseMpegChannelConfiguration(xpp); + break; + case "tag:dolby.com,2014:dash:audio_channel_configuration:2011": + case "urn:dolby:dash:audio_channel_configuration:2011": + audioChannels = parseDolbyChannelConfiguration(xpp); + break; + default: + audioChannels = Format.NO_VALUE; + break; + } do { xpp.next(); } while (!XmlPullParserUtil.isEndTag(xpp, "AudioChannelConfiguration")); @@ -1528,6 +1547,21 @@ public class DashManifestParser extends DefaultHandler return value == null ? defaultValue : value; } + /** + * Parses the number of channels from the value attribute of an AudioElementConfiguration with + * schemeIdUri "urn:mpeg:mpegB:cicp:ChannelConfiguration", as defined by ISO 23001-8 clause 8.1. + * + * @param xpp The parser from which to read. + * @return The parsed number of channels, or {@link Format#NO_VALUE} if the channel count could + * not be parsed. + */ + protected static int parseMpegChannelConfiguration(XmlPullParser xpp) { + int index = parseInt(xpp, "value", C.INDEX_UNSET); + return 0 <= index && index < MPEG_CHANNEL_CONFIGURATION_MAPPING.length + ? MPEG_CHANNEL_CONFIGURATION_MAPPING[index] + : Format.NO_VALUE; + } + /** * Parses the number of channels from the value attribute of an AudioElementConfiguration with * schemeIdUri "tag:dolby.com,2014:dash:audio_channel_configuration:2011", as defined by table E.5 From a1999ef854d52e65ceaf25915a2f759580cd4c32 Mon Sep 17 00:00:00 2001 From: christosts Date: Thu, 24 Sep 2020 14:51:02 +0100 Subject: [PATCH 062/110] Util.getStringForTime() prefixes negative times Fix bug to place the negative sign in the beginning of the returned String. PiperOrigin-RevId: 333504868 --- .../java/com/google/android/exoplayer2/util/Util.java | 8 ++++++-- .../java/com/google/android/exoplayer2/util/UtilTest.java | 8 ++++++++ 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/library/common/src/main/java/com/google/android/exoplayer2/util/Util.java b/library/common/src/main/java/com/google/android/exoplayer2/util/Util.java index f954b60c45..5505649cf0 100644 --- a/library/common/src/main/java/com/google/android/exoplayer2/util/Util.java +++ b/library/common/src/main/java/com/google/android/exoplayer2/util/Util.java @@ -17,6 +17,7 @@ package com.google.android.exoplayer2.util; import static android.content.Context.UI_MODE_SERVICE; import static com.google.android.exoplayer2.util.Assertions.checkNotNull; +import static java.lang.Math.abs; import static java.lang.Math.max; import static java.lang.Math.min; @@ -1844,13 +1845,16 @@ public final class Util { if (timeMs == C.TIME_UNSET) { timeMs = 0; } + String prefix = timeMs < 0 ? "-" : ""; + timeMs = abs(timeMs); long totalSeconds = (timeMs + 500) / 1000; long seconds = totalSeconds % 60; long minutes = (totalSeconds / 60) % 60; long hours = totalSeconds / 3600; builder.setLength(0); - return hours > 0 ? formatter.format("%d:%02d:%02d", hours, minutes, seconds).toString() - : formatter.format("%02d:%02d", minutes, seconds).toString(); + return hours > 0 + ? formatter.format("%s%d:%02d:%02d", prefix, hours, minutes, seconds).toString() + : formatter.format("%s%02d:%02d", prefix, minutes, seconds).toString(); } /** diff --git a/library/common/src/test/java/com/google/android/exoplayer2/util/UtilTest.java b/library/common/src/test/java/com/google/android/exoplayer2/util/UtilTest.java index 162dcbae9d..cda9e054f1 100644 --- a/library/common/src/test/java/com/google/android/exoplayer2/util/UtilTest.java +++ b/library/common/src/test/java/com/google/android/exoplayer2/util/UtilTest.java @@ -19,6 +19,7 @@ import static com.google.android.exoplayer2.util.Util.binarySearchCeil; import static com.google.android.exoplayer2.util.Util.binarySearchFloor; import static com.google.android.exoplayer2.util.Util.escapeFileName; import static com.google.android.exoplayer2.util.Util.getCodecsOfType; +import static com.google.android.exoplayer2.util.Util.getStringForTime; import static com.google.android.exoplayer2.util.Util.parseXsDateTime; import static com.google.android.exoplayer2.util.Util.parseXsDuration; import static com.google.android.exoplayer2.util.Util.unescapeFileName; @@ -37,6 +38,7 @@ import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.util.ArrayList; import java.util.Arrays; +import java.util.Formatter; import java.util.Random; import java.util.zip.Deflater; import org.junit.Test; @@ -1082,6 +1084,12 @@ public class UtilTest { assertThat(Util.tableExists(database, "table")).isFalse(); } + @Test + public void getStringForTime_withNegativeTime_setsNegativePrefix() { + assertThat(getStringForTime(new StringBuilder(), new Formatter(), /* timeMs= */ -35000)) + .isEqualTo("-00:35"); + } + private static void assertEscapeUnescapeFileName(String fileName, String escapedFileName) { assertThat(escapeFileName(fileName)).isEqualTo(escapedFileName); assertThat(unescapeFileName(escapedFileName)).isEqualTo(fileName); From 973d23543ef97bdaa75b1876381b4155f07beb6e Mon Sep 17 00:00:00 2001 From: samrobinson Date: Fri, 25 Sep 2020 12:36:10 +0100 Subject: [PATCH 063/110] Add support for mp2 boxes. Issue: #7967 PiperOrigin-RevId: 333709003 --- RELEASENOTES.md | 3 +++ .../java/com/google/android/exoplayer2/extractor/mp4/Atom.java | 3 +++ .../google/android/exoplayer2/extractor/mp4/AtomParsers.java | 3 ++- 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index dbeb77236a..9353d1e73a 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -11,6 +11,9 @@ ([#7866](https://github.com/google/ExoPlayer/issues/7866)). * Text: * Add support for `\h` SSA/ASS style override code (non-breaking space). +* Extractors: + * Add support for .mp2 boxes in the `AtomParsers` + ([#7967](https://github.com/google/ExoPlayer/issues/7967)). ### 2.12.0 (2020-09-11) ### diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mp4/Atom.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mp4/Atom.java index 58f3a75b87..325dc24aec 100644 --- a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mp4/Atom.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mp4/Atom.java @@ -115,6 +115,9 @@ import java.util.List; @SuppressWarnings("ConstantCaseForConstants") public static final int TYPE_mp4a = 0x6d703461; + @SuppressWarnings("ConstantCaseForConstants") + public static final int TYPE__mp2 = 0x2e6d7032; + @SuppressWarnings("ConstantCaseForConstants") public static final int TYPE__mp3 = 0x2e6d7033; diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java index 0ab126367b..573451ef6a 100644 --- a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mp4/AtomParsers.java @@ -878,6 +878,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; || childAtomType == Atom.TYPE_lpcm || childAtomType == Atom.TYPE_sowt || childAtomType == Atom.TYPE_twos + || childAtomType == Atom.TYPE__mp2 || childAtomType == Atom.TYPE__mp3 || childAtomType == Atom.TYPE_alac || childAtomType == Atom.TYPE_alaw @@ -1243,7 +1244,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; } else if (atomType == Atom.TYPE_twos) { mimeType = MimeTypes.AUDIO_RAW; pcmEncoding = C.ENCODING_PCM_16BIT_BIG_ENDIAN; - } else if (atomType == Atom.TYPE__mp3) { + } else if (atomType == Atom.TYPE__mp2 || atomType == Atom.TYPE__mp3) { mimeType = MimeTypes.AUDIO_MPEG; } else if (atomType == Atom.TYPE_alac) { mimeType = MimeTypes.AUDIO_ALAC; From 300bee5f0b727d470c8a705cfdd3e245d5ca4fcc Mon Sep 17 00:00:00 2001 From: christosts Date: Fri, 25 Sep 2020 13:10:35 +0100 Subject: [PATCH 064/110] Bring back setRenderTimeLimitMs PiperOrigin-RevId: 333712782 --- .../mediacodec/MediaCodecRenderer.java | 26 +++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java index de3f595976..a2ba72dbc0 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/mediacodec/MediaCodecRenderer.java @@ -363,6 +363,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer { @Nullable private DrmSession sourceDrmSession; @Nullable private MediaCrypto mediaCrypto; private boolean mediaCryptoRequiresSecureDecoder; + private long renderTimeLimitMs; private float operatingRate; @Nullable private MediaCodec codec; @Nullable private MediaCodecAdapter codecAdapter; @@ -442,6 +443,7 @@ public abstract class MediaCodecRenderer extends BaseRenderer { outputBufferInfo = new MediaCodec.BufferInfo(); operatingRate = 1f; mediaCodecOperationMode = OPERATION_MODE_SYNCHRONOUS; + renderTimeLimitMs = C.TIME_UNSET; pendingOutputStreamStartPositionsUs = new long[MAX_PENDING_OUTPUT_STREAM_OFFSET_COUNT]; pendingOutputStreamOffsetsUs = new long[MAX_PENDING_OUTPUT_STREAM_OFFSET_COUNT]; pendingOutputStreamSwitchTimesUs = new long[MAX_PENDING_OUTPUT_STREAM_OFFSET_COUNT]; @@ -451,6 +453,19 @@ public abstract class MediaCodecRenderer extends BaseRenderer { resetCodecStateForRelease(); } + /** + * Set a limit on the time a single {@link #render(long, long)} call can spend draining and + * filling the decoder. + * + *

    This method should be called right after creating an instance of this class. + * + * @param renderTimeLimitMs The render time limit in milliseconds, or {@link C#TIME_UNSET} for no + * limit. + */ + public void setRenderTimeLimitMs(long renderTimeLimitMs) { + this.renderTimeLimitMs = renderTimeLimitMs; + } + /** * Set the mode of operation of the underlying {@link MediaCodec}. * @@ -837,9 +852,11 @@ public abstract class MediaCodecRenderer extends BaseRenderer { while (bypassRender(positionUs, elapsedRealtimeUs)) {} TraceUtil.endSection(); } else if (codec != null) { + long renderStartTimeMs = SystemClock.elapsedRealtime(); TraceUtil.beginSection("drainAndFeed"); - while (drainOutputBuffer(positionUs, elapsedRealtimeUs)) {} - while (feedInputBuffer()) {} + while (drainOutputBuffer(positionUs, elapsedRealtimeUs) + && shouldContinueRendering(renderStartTimeMs)) {} + while (feedInputBuffer() && shouldContinueRendering(renderStartTimeMs)) {} TraceUtil.endSection(); } else { decoderCounters.skippedInputBufferCount += skipSource(positionUs); @@ -1171,6 +1188,11 @@ public abstract class MediaCodecRenderer extends BaseRenderer { onCodecInitialized(codecName, codecInitializedTimestamp, elapsed); } + private boolean shouldContinueRendering(long renderStartTimeMs) { + return renderTimeLimitMs == C.TIME_UNSET + || SystemClock.elapsedRealtime() - renderStartTimeMs < renderTimeLimitMs; + } + private void getCodecBuffers(MediaCodec codec) { if (Util.SDK_INT < 21) { inputBuffers = codec.getInputBuffers(); From 19530866c72801d96e929164e957b3677e76999e Mon Sep 17 00:00:00 2001 From: bachinger Date: Fri, 25 Sep 2020 15:08:08 +0100 Subject: [PATCH 065/110] Always pass true for ongoing with the first notification ISSUE: #7977 PiperOrigin-RevId: 333726625 --- .../android/exoplayer2/ui/PlayerNotificationManager.java | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerNotificationManager.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerNotificationManager.java index e23c91cd16..b52a3e6f82 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerNotificationManager.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/PlayerNotificationManager.java @@ -989,7 +989,6 @@ public class PlayerNotificationManager { Notification notification = builder.build(); notificationManager.notify(notificationId, notification); if (!isNotificationStarted) { - isNotificationStarted = true; context.registerReceiver(notificationBroadcastReceiver, intentFilter); if (notificationListener != null) { notificationListener.onNotificationStarted(notificationId, notification); @@ -997,8 +996,12 @@ public class PlayerNotificationManager { } @Nullable NotificationListener listener = notificationListener; if (listener != null) { - listener.onNotificationPosted(notificationId, notification, ongoing); + // Always pass true for ongoing with the first notification to tell a service to go into + // foreground even when paused. + listener.onNotificationPosted( + notificationId, notification, ongoing || !isNotificationStarted); } + isNotificationStarted = true; } // We're calling a deprecated listener method that we still want to notify. From 57f11d155613ddb9c9f59c8c6f0b78f7d55ca1f6 Mon Sep 17 00:00:00 2001 From: kimvde Date: Fri, 25 Sep 2020 16:01:54 +0100 Subject: [PATCH 066/110] Parse TLEN duration in Mp3Extractor Issue: #7949 PiperOrigin-RevId: 333733615 --- RELEASENOTES.md | 2 ++ .../exoplayer2/extractor/mp3/MlltSeeker.java | 17 +++++++++------ .../extractor/mp3/Mp3Extractor.java | 21 +++++++++++++++++-- 3 files changed, 32 insertions(+), 8 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 9353d1e73a..c0c78f9ac2 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -14,6 +14,8 @@ * Extractors: * Add support for .mp2 boxes in the `AtomParsers` ([#7967](https://github.com/google/ExoPlayer/issues/7967)). + * Use TLEN ID3 tag to compute the duration in Mp3Extractor + ([#7949](https://github.com/google/ExoPlayer/issues/7949)). ### 2.12.0 (2020-09-11) ### diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mp3/MlltSeeker.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mp3/MlltSeeker.java index 1b627483f0..f30b830249 100644 --- a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mp3/MlltSeeker.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mp3/MlltSeeker.java @@ -29,9 +29,11 @@ import com.google.android.exoplayer2.util.Util; * * @param firstFramePosition The position of the start of the first frame in the stream. * @param mlltFrame The MLLT frame with seeking metadata. + * @param durationUs The stream duration in microseconds, or {@link C#TIME_UNSET} if it is + * unknown. * @return An {@link MlltSeeker} for seeking in the stream. */ - public static MlltSeeker create(long firstFramePosition, MlltFrame mlltFrame) { + public static MlltSeeker create(long firstFramePosition, MlltFrame mlltFrame, long durationUs) { int referenceCount = mlltFrame.bytesDeviations.length; long[] referencePositions = new long[1 + referenceCount]; long[] referenceTimesMs = new long[1 + referenceCount]; @@ -45,19 +47,22 @@ import com.google.android.exoplayer2.util.Util; referencePositions[i] = position; referenceTimesMs[i] = timeMs; } - return new MlltSeeker(referencePositions, referenceTimesMs); + return new MlltSeeker(referencePositions, referenceTimesMs, durationUs); } private final long[] referencePositions; private final long[] referenceTimesMs; private final long durationUs; - private MlltSeeker(long[] referencePositions, long[] referenceTimesMs) { + private MlltSeeker(long[] referencePositions, long[] referenceTimesMs, long durationUs) { this.referencePositions = referencePositions; this.referenceTimesMs = referenceTimesMs; - // Use the last reference point as the duration, as extrapolating variable bitrate at the end of - // the stream may give a large error. - durationUs = C.msToUs(referenceTimesMs[referenceTimesMs.length - 1]); + // Use the last reference point as the duration if it is unknown, as extrapolating variable + // bitrate at the end of the stream may give a large error. + this.durationUs = + durationUs != C.TIME_UNSET + ? durationUs + : C.msToUs(referenceTimesMs[referenceTimesMs.length - 1]); } @Override diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mp3/Mp3Extractor.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mp3/Mp3Extractor.java index 59d128ab9b..c2aba6d7bd 100644 --- a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mp3/Mp3Extractor.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/mp3/Mp3Extractor.java @@ -35,6 +35,7 @@ import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.metadata.id3.Id3Decoder; import com.google.android.exoplayer2.metadata.id3.Id3Decoder.FramePredicate; import com.google.android.exoplayer2.metadata.id3.MlltFrame; +import com.google.android.exoplayer2.metadata.id3.TextInformationFrame; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.ParsableByteArray; import com.google.android.exoplayer2.util.Util; @@ -432,7 +433,7 @@ public final class Mp3Extractor implements Extractor { @Nullable Seeker resultSeeker = null; if ((flags & FLAG_ENABLE_INDEX_SEEKING) != 0) { - long durationUs = C.TIME_UNSET; + long durationUs; long dataEndPosition = C.POSITION_UNSET; if (metadataSeeker != null) { durationUs = metadataSeeker.getDurationUs(); @@ -440,6 +441,8 @@ public final class Mp3Extractor implements Extractor { } else if (seekFrameSeeker != null) { durationUs = seekFrameSeeker.getDurationUs(); dataEndPosition = seekFrameSeeker.getDataEndPosition(); + } else { + durationUs = getId3TlenUs(metadata); } resultSeeker = new IndexSeeker( @@ -554,10 +557,24 @@ public final class Mp3Extractor implements Extractor { for (int i = 0; i < length; i++) { Metadata.Entry entry = metadata.get(i); if (entry instanceof MlltFrame) { - return MlltSeeker.create(firstFramePosition, (MlltFrame) entry); + return MlltSeeker.create(firstFramePosition, (MlltFrame) entry, getId3TlenUs(metadata)); } } } return null; } + + private static long getId3TlenUs(@Nullable Metadata metadata) { + if (metadata != null) { + int length = metadata.length(); + for (int i = 0; i < length; i++) { + Metadata.Entry entry = metadata.get(i); + if (entry instanceof TextInformationFrame + && ((TextInformationFrame) entry).id.equals("TLEN")) { + return C.msToUs(Long.parseLong(((TextInformationFrame) entry).value)); + } + } + } + return C.TIME_UNSET; + } } From 9819664bd15ab221bb268c8d6273e2d8638d699c Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Fri, 25 Sep 2020 17:49:12 +0100 Subject: [PATCH 067/110] Merge pull request #7798 from yoobi:trackSelectionView PiperOrigin-RevId: 333751261 --- RELEASENOTES.md | 4 ++ .../exoplayer2/demo/TrackSelectionDialog.java | 7 ++- .../ui/TrackSelectionDialogBuilder.java | 21 +++++++- .../exoplayer2/ui/TrackSelectionView.java | 50 ++++++++++++++++--- 4 files changed, 72 insertions(+), 10 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index c0c78f9ac2..8b95794e93 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -16,6 +16,10 @@ ([#7967](https://github.com/google/ExoPlayer/issues/7967)). * Use TLEN ID3 tag to compute the duration in Mp3Extractor ([#7949](https://github.com/google/ExoPlayer/issues/7949)). +* UI + * Add the option to sort tracks by `Format` in `TrackSelectionView` and + `TrackSelectionDialogBuilder` + ([#7709](https://github.com/google/ExoPlayer/issues/7709)). ### 2.12.0 (2020-09-11) ### diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/TrackSelectionDialog.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/TrackSelectionDialog.java index 5cf2353f21..d3f9b3880d 100644 --- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/TrackSelectionDialog.java +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/TrackSelectionDialog.java @@ -354,7 +354,12 @@ public final class TrackSelectionDialog extends DialogFragment { trackSelectionView.setAllowMultipleOverrides(allowMultipleOverrides); trackSelectionView.setAllowAdaptiveSelections(allowAdaptiveSelections); trackSelectionView.init( - mappedTrackInfo, rendererIndex, isDisabled, overrides, /* listener= */ this); + mappedTrackInfo, + rendererIndex, + isDisabled, + overrides, + /* trackFormatComparator= */ null, + /* listener= */ this); return rootView; } diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/TrackSelectionDialogBuilder.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/TrackSelectionDialogBuilder.java index 520b2d7580..be3fb9bc90 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/TrackSelectionDialogBuilder.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/TrackSelectionDialogBuilder.java @@ -25,6 +25,7 @@ import android.view.LayoutInflater; import android.view.View; import androidx.annotation.Nullable; import androidx.annotation.StyleRes; +import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; import com.google.android.exoplayer2.trackselection.DefaultTrackSelector.SelectionOverride; @@ -32,6 +33,7 @@ import com.google.android.exoplayer2.trackselection.MappingTrackSelector.MappedT import com.google.android.exoplayer2.trackselection.TrackSelectionUtil; import java.lang.reflect.Constructor; import java.util.Collections; +import java.util.Comparator; import java.util.List; /** Builder for a dialog with a {@link TrackSelectionView}. */ @@ -62,6 +64,7 @@ public final class TrackSelectionDialogBuilder { @Nullable private TrackNameProvider trackNameProvider; private boolean isDisabled; private List overrides; + @Nullable private Comparator trackFormatComparator; /** * Creates a builder for a track selection dialog. @@ -208,6 +211,16 @@ public final class TrackSelectionDialogBuilder { return this; } + /** + * Sets a {@link Comparator} used to determine the display order of the tracks within each track + * group. + * + * @param trackFormatComparator The comparator, or {@code null} to use the original order. + */ + public void setTrackFormatComparator(@Nullable Comparator trackFormatComparator) { + this.trackFormatComparator = trackFormatComparator; + } + /** * Sets the {@link TrackNameProvider} used to generate the user visible name of each track and * updates the view with track names queried from the specified provider. @@ -287,7 +300,13 @@ public final class TrackSelectionDialogBuilder { if (trackNameProvider != null) { selectionView.setTrackNameProvider(trackNameProvider); } - selectionView.init(mappedTrackInfo, rendererIndex, isDisabled, overrides, /* listener= */ null); + selectionView.init( + mappedTrackInfo, + rendererIndex, + isDisabled, + overrides, + trackFormatComparator, + /* listener= */ null); return (dialog, which) -> callback.onTracksSelected(selectionView.getIsDisabled(), selectionView.getOverrides()); } diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/TrackSelectionView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/TrackSelectionView.java index b47feb2a71..8a8f3d3c76 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/TrackSelectionView.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/TrackSelectionView.java @@ -18,7 +18,6 @@ package com.google.android.exoplayer2.ui; import android.content.Context; import android.content.res.TypedArray; import android.util.AttributeSet; -import android.util.Pair; import android.util.SparseArray; import android.view.LayoutInflater; import android.view.View; @@ -26,6 +25,7 @@ import android.widget.CheckedTextView; import android.widget.LinearLayout; import androidx.annotation.AttrRes; import androidx.annotation.Nullable; +import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.RendererCapabilities; import com.google.android.exoplayer2.source.TrackGroup; import com.google.android.exoplayer2.source.TrackGroupArray; @@ -35,6 +35,7 @@ import com.google.android.exoplayer2.trackselection.MappingTrackSelector.MappedT import com.google.android.exoplayer2.util.Assertions; import java.util.ArrayList; import java.util.Arrays; +import java.util.Comparator; import java.util.List; import org.checkerframework.checker.nullness.qual.MonotonicNonNull; import org.checkerframework.checker.nullness.qual.RequiresNonNull; @@ -71,6 +72,7 @@ public class TrackSelectionView extends LinearLayout { private int rendererIndex; private TrackGroupArray trackGroups; private boolean isDisabled; + @Nullable private Comparator trackInfoComparator; @Nullable private TrackSelectionListener listener; /** Creates a track selection view. */ @@ -196,6 +198,8 @@ public class TrackSelectionView extends LinearLayout { * @param overrides List of initial overrides to be shown for this renderer. There must be at most * one override for each track group. If {@link #setAllowMultipleOverrides(boolean)} hasn't * been set to {@code true}, only the first override is used. + * @param trackFormatComparator An optional comparator used to determine the display order of the + * tracks within each track group. * @param listener An optional listener for track selection updates. */ public void init( @@ -203,10 +207,15 @@ public class TrackSelectionView extends LinearLayout { int rendererIndex, boolean isDisabled, List overrides, + @Nullable Comparator trackFormatComparator, @Nullable TrackSelectionListener listener) { this.mappedTrackInfo = mappedTrackInfo; this.rendererIndex = rendererIndex; this.isDisabled = isDisabled; + this.trackInfoComparator = + trackFormatComparator == null + ? null + : (o1, o2) -> trackFormatComparator.compare(o1.format, o2.format); this.listener = listener; int maxOverrides = allowMultipleOverrides ? overrides.size() : Math.min(overrides.size(), 1); for (int i = 0; i < maxOverrides; i++) { @@ -259,7 +268,16 @@ public class TrackSelectionView extends LinearLayout { TrackGroup group = trackGroups.get(groupIndex); boolean enableMultipleChoiceForAdaptiveSelections = shouldEnableAdaptiveSelection(groupIndex); trackViews[groupIndex] = new CheckedTextView[group.length]; + + TrackInfo[] trackInfos = new TrackInfo[group.length]; for (int trackIndex = 0; trackIndex < group.length; trackIndex++) { + trackInfos[trackIndex] = new TrackInfo(groupIndex, trackIndex, group.getFormat(trackIndex)); + } + if (trackInfoComparator != null) { + Arrays.sort(trackInfos, trackInfoComparator); + } + + for (int trackIndex = 0; trackIndex < trackInfos.length; trackIndex++) { if (trackIndex == 0) { addView(inflater.inflate(R.layout.exo_list_divider, this, false)); } @@ -270,11 +288,11 @@ public class TrackSelectionView extends LinearLayout { CheckedTextView trackView = (CheckedTextView) inflater.inflate(trackViewLayoutId, this, false); trackView.setBackgroundResource(selectableItemBackgroundResourceId); - trackView.setText(trackNameProvider.getTrackName(group.getFormat(trackIndex))); + trackView.setText(trackNameProvider.getTrackName(trackInfos[trackIndex].format)); if (mappedTrackInfo.getTrackSupport(rendererIndex, groupIndex, trackIndex) == RendererCapabilities.FORMAT_HANDLED) { trackView.setFocusable(true); - trackView.setTag(Pair.create(groupIndex, trackIndex)); + trackView.setTag(trackInfos[trackIndex]); trackView.setOnClickListener(componentListener); } else { trackView.setFocusable(false); @@ -294,7 +312,12 @@ public class TrackSelectionView extends LinearLayout { for (int i = 0; i < trackViews.length; i++) { SelectionOverride override = overrides.get(i); for (int j = 0; j < trackViews[i].length; j++) { - trackViews[i][j].setChecked(override != null && override.containsTrack(j)); + if (override != null) { + TrackInfo trackInfo = (TrackInfo) Assertions.checkNotNull(trackViews[i][j].getTag()); + trackViews[i][j].setChecked(override.containsTrack(trackInfo.trackIndex)); + } else { + trackViews[i][j].setChecked(false); + } } } } @@ -325,10 +348,9 @@ public class TrackSelectionView extends LinearLayout { private void onTrackViewClicked(View view) { isDisabled = false; - @SuppressWarnings("unchecked") - Pair tag = (Pair) Assertions.checkNotNull(view.getTag()); - int groupIndex = tag.first; - int trackIndex = tag.second; + TrackInfo trackInfo = (TrackInfo) Assertions.checkNotNull(view.getTag()); + int groupIndex = trackInfo.groupIndex; + int trackIndex = trackInfo.trackIndex; SelectionOverride override = overrides.get(groupIndex); Assertions.checkNotNull(mappedTrackInfo); if (override == null) { @@ -406,4 +428,16 @@ public class TrackSelectionView extends LinearLayout { TrackSelectionView.this.onClick(view); } } + + private static final class TrackInfo { + public final int groupIndex; + public final int trackIndex; + public final Format format; + + public TrackInfo(int groupIndex, int trackIndex, Format format) { + this.groupIndex = groupIndex; + this.trackIndex = trackIndex; + this.format = format; + } + } } From 1bdccd4bfb3d9d23543c138445215beb6d190856 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Mon, 28 Sep 2020 10:27:18 +0100 Subject: [PATCH 068/110] Fix position reporting with fetch errors On receiving a fetch error for an ad that would otherwise play based on an initial/seek position, the pending content position wasn't cleared which meant that position reporting was broken after a fetch error. Fix this by always clearing the pending position (if there was a pending position that will have triggered the fetch error). Also deduplicate the code for handling empty ad groups (fetch errors) and ad group load errors. Issue: #7956 PiperOrigin-RevId: 334113131 --- RELEASENOTES.md | 3 ++ .../exoplayer2/ext/ima/ImaAdsLoader.java | 41 ++++++------------- .../exoplayer2/ext/ima/ImaAdsLoaderTest.java | 26 ++++++++++++ 3 files changed, 42 insertions(+), 28 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 8b95794e93..8ffa7562e8 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -20,6 +20,9 @@ * Add the option to sort tracks by `Format` in `TrackSelectionView` and `TrackSelectionDialogBuilder` ([#7709](https://github.com/google/ExoPlayer/issues/7709)). +* IMA extension: + * Fix position reporting after fetch errors + ([#7956](https://github.com/google/ExoPlayer/issues/7956)). ### 2.12.0 (2020-09-11) ### diff --git a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java index 88b0daac49..cf8d487ede 100644 --- a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java +++ b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java @@ -1077,7 +1077,7 @@ public final class ImaAdsLoader adGroupTimeSeconds == -1.0 ? adPlaybackState.adGroupCount - 1 : getAdGroupIndexForCuePointTimeSeconds(adGroupTimeSeconds); - handleAdGroupFetchError(adGroupIndex); + markAdGroupInErrorStateAndClearPendingContentPosition(adGroupIndex); break; case CONTENT_PAUSE_REQUESTED: // After CONTENT_PAUSE_REQUESTED, IMA will playAd/pauseAd/stopAd to show one or more ads @@ -1364,35 +1364,20 @@ public final class ImaAdsLoader } } - private void handleAdGroupFetchError(int adGroupIndex) { - AdPlaybackState.AdGroup adGroup = adPlaybackState.adGroups[adGroupIndex]; - if (adGroup.count == C.LENGTH_UNSET) { - adPlaybackState = adPlaybackState.withAdCount(adGroupIndex, max(1, adGroup.states.length)); - adGroup = adPlaybackState.adGroups[adGroupIndex]; - } - for (int i = 0; i < adGroup.count; i++) { - if (adGroup.states[i] == AdPlaybackState.AD_STATE_UNAVAILABLE) { - if (DEBUG) { - Log.d(TAG, "Removing ad " + i + " in ad group " + adGroupIndex); - } - adPlaybackState = adPlaybackState.withAdLoadError(adGroupIndex, i); - } - } - updateAdPlaybackState(); - } - private void handleAdGroupLoadError(Exception error) { - if (player == null) { - return; - } - - // TODO: Once IMA signals which ad group failed to load, remove this call. int adGroupIndex = getLoadingAdGroupIndex(); if (adGroupIndex == C.INDEX_UNSET) { Log.w(TAG, "Unable to determine ad group index for ad group load error", error); return; } + markAdGroupInErrorStateAndClearPendingContentPosition(adGroupIndex); + if (pendingAdLoadError == null) { + pendingAdLoadError = AdLoadException.createForAdGroup(error, adGroupIndex); + } + } + private void markAdGroupInErrorStateAndClearPendingContentPosition(int adGroupIndex) { + // Update the ad playback state so all ads in the ad group are in the error state. AdPlaybackState.AdGroup adGroup = adPlaybackState.adGroups[adGroupIndex]; if (adGroup.count == C.LENGTH_UNSET) { adPlaybackState = adPlaybackState.withAdCount(adGroupIndex, max(1, adGroup.states.length)); @@ -1407,9 +1392,7 @@ public final class ImaAdsLoader } } updateAdPlaybackState(); - if (pendingAdLoadError == null) { - pendingAdLoadError = AdLoadException.createForAdGroup(error, adGroupIndex); - } + // Clear any pending content position that triggered attempting to load the ad group. pendingContentPositionMs = C.TIME_UNSET; fakeContentProgressElapsedRealtimeMs = C.TIME_UNSET; } @@ -1522,8 +1505,10 @@ public final class ImaAdsLoader * no such ad group. */ private int getLoadingAdGroupIndex() { - long playerPositionUs = - C.msToUs(getContentPeriodPositionMs(checkNotNull(player), timeline, period)); + if (player == null) { + return C.INDEX_UNSET; + } + long playerPositionUs = C.msToUs(getContentPeriodPositionMs(player, timeline, period)); int adGroupIndex = adPlaybackState.getAdGroupIndexForPositionUs(playerPositionUs, C.msToUs(contentDurationMs)); if (adGroupIndex == C.INDEX_UNSET) { diff --git a/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoaderTest.java b/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoaderTest.java index e32a199200..c2cc384888 100644 --- a/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoaderTest.java +++ b/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoaderTest.java @@ -48,6 +48,7 @@ import com.google.ads.interactivemedia.v3.api.ImaSdkSettings; import com.google.ads.interactivemedia.v3.api.player.AdMediaInfo; import com.google.ads.interactivemedia.v3.api.player.ContentProgressProvider; import com.google.ads.interactivemedia.v3.api.player.VideoAdPlayer; +import com.google.ads.interactivemedia.v3.api.player.VideoProgressUpdate; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.MediaItem; @@ -311,6 +312,31 @@ public final class ImaAdsLoaderTest { .withAdLoadError(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0)); } + @Test + public void playback_withMidrollFetchError_updatesContentProgress() { + AdEvent mockMidrollFetchErrorAdEvent = mock(AdEvent.class); + when(mockMidrollFetchErrorAdEvent.getType()).thenReturn(AdEventType.AD_BREAK_FETCH_ERROR); + when(mockMidrollFetchErrorAdEvent.getAdData()) + .thenReturn(ImmutableMap.of("adBreakTime", "5.5")); + setupPlayback(CONTENT_TIMELINE, ImmutableList.of(5.5f)); + + // Simulate loading an empty midroll ad and advancing the player position. + imaAdsLoader.start(adsLoaderListener, adViewProvider); + adEventListener.onAdEvent(mockMidrollFetchErrorAdEvent); + long playerPositionUs = CONTENT_DURATION_US - C.MICROS_PER_SECOND; + long playerPositionInPeriodUs = + playerPositionUs + TimelineWindowDefinition.DEFAULT_WINDOW_OFFSET_IN_FIRST_PERIOD_US; + long periodDurationUs = + CONTENT_TIMELINE.getPeriod(/* periodIndex= */ 0, new Period()).durationUs; + fakeExoPlayer.setPlayingContentPosition(C.usToMs(playerPositionUs)); + + // Verify the content progress is updated to reflect the new player position. + assertThat(contentProgressProvider.getContentProgress()) + .isEqualTo( + new VideoProgressUpdate( + C.usToMs(playerPositionInPeriodUs), C.usToMs(periodDurationUs))); + } + @Test public void playback_withPostrollFetchError_marksAdAsInErrorState() { AdEvent mockPostrollFetchErrorAdEvent = mock(AdEvent.class); From 89cd796bf68fd72aa20e5c46f4d1d85fcaf13f52 Mon Sep 17 00:00:00 2001 From: samrobinson Date: Mon, 28 Sep 2020 11:45:00 +0100 Subject: [PATCH 069/110] Do not require subtitleButton in custom layouts of StyledPlayerView Every other subtitleButton has an if not null check, but does not force non null. Issue: #7962 PiperOrigin-RevId: 334124323 --- RELEASENOTES.md | 3 +++ .../exoplayer2/ui/StyledPlayerControlView.java | 12 +++++++----- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 8ffa7562e8..e7f88cba51 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -11,6 +11,9 @@ ([#7866](https://github.com/google/ExoPlayer/issues/7866)). * Text: * Add support for `\h` SSA/ASS style override code (non-breaking space). +* UI: + * Do not require subtitleButton in custom layouts of StyledPlayerView + ([#7962](https://github.com/google/ExoPlayer/issues/7962)). * Extractors: * Add support for .mp2 boxes in the `AtomParsers` ([#7967](https://github.com/google/ExoPlayer/issues/7967)). diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/StyledPlayerControlView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/StyledPlayerControlView.java index 8bb9babeb0..c3add8f8af 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/StyledPlayerControlView.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/StyledPlayerControlView.java @@ -2005,11 +2005,13 @@ public class StyledPlayerControlView extends FrameLayout { break; } } - checkNotNull(subtitleButton) - .setImageDrawable(subtitleIsOn ? subtitleOnButtonDrawable : subtitleOffButtonDrawable); - checkNotNull(subtitleButton) - .setContentDescription( - subtitleIsOn ? subtitleOnContentDescription : subtitleOffContentDescription); + + if (subtitleButton != null) { + subtitleButton.setImageDrawable( + subtitleIsOn ? subtitleOnButtonDrawable : subtitleOffButtonDrawable); + subtitleButton.setContentDescription( + subtitleIsOn ? subtitleOnContentDescription : subtitleOffContentDescription); + } this.rendererIndices = rendererIndices; this.tracks = trackInfos; this.mappedTrackInfo = mappedTrackInfo; From 824b2a7305bd2b551c2dea6d036cb61fabe6c3ad Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Mon, 28 Sep 2020 15:15:26 +0100 Subject: [PATCH 070/110] Fix position ramping behavior with AudioTrack speed params Non-realtime AudioTrack playback speed was not taken into account when extrapolating the old mode's position, causing the position not to advance smoothly. This should be a no-op when not using AudioTrack playback params for speed adjustment. Issue: #7982 PiperOrigin-RevId: 334151163 --- RELEASENOTES.md | 4 ++++ .../android/exoplayer2/audio/AudioTrackPositionTracker.java | 5 ++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index e7f88cba51..55aaa5d486 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -14,6 +14,10 @@ * UI: * Do not require subtitleButton in custom layouts of StyledPlayerView ([#7962](https://github.com/google/ExoPlayer/issues/7962)). +* Audio: + * Fix the default audio sink position not advancing correctly when using + `AudioTrack`-based speed adjustment + ([#7982](https://github.com/google/ExoPlayer/issues/7982)). * Extractors: * Add support for .mp2 boxes in the `AtomParsers` ([#7967](https://github.com/google/ExoPlayer/issues/7967)). diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioTrackPositionTracker.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioTrackPositionTracker.java index 540ee098ee..8891a6d8d1 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioTrackPositionTracker.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioTrackPositionTracker.java @@ -289,7 +289,10 @@ import java.lang.reflect.Method; if (elapsedSincePreviousModeUs < MODE_SWITCH_SMOOTHING_DURATION_US) { // Use a ramp to smooth between the old mode and the new one to avoid introducing a sudden // jump if the two modes disagree. - long previousModeProjectedPositionUs = previousModePositionUs + elapsedSincePreviousModeUs; + long previousModeProjectedPositionUs = + previousModePositionUs + + Util.getMediaDurationForPlayoutDuration( + elapsedSincePreviousModeUs, audioTrackPlaybackSpeed); // A ramp consisting of 1000 points distributed over MODE_SWITCH_SMOOTHING_DURATION_US. long rampPoint = (elapsedSincePreviousModeUs * 1000) / MODE_SWITCH_SMOOTHING_DURATION_US; positionUs *= rampPoint; From 358d205f0fea9879ee6db092328b070274de7776 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Wed, 30 Sep 2020 11:08:04 +0100 Subject: [PATCH 071/110] Use Builder in ImaAdsLoader constructor PiperOrigin-RevId: 334562209 --- .../exoplayer2/ext/ima/ImaAdsLoader.java | 94 ++++--------------- 1 file changed, 18 insertions(+), 76 deletions(-) diff --git a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java index cf8d487ede..157fab938c 100644 --- a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java +++ b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java @@ -142,7 +142,7 @@ public final class ImaAdsLoader * @param context The context; */ public Builder(Context context) { - this.context = checkNotNull(context); + this.context = checkNotNull(context).getApplicationContext(); adPreloadTimeoutMs = DEFAULT_AD_PRELOAD_TIMEOUT_MS; vastLoadTimeoutMs = TIMEOUT_UNSET; mediaLoadTimeoutMs = TIMEOUT_UNSET; @@ -318,21 +318,7 @@ public final class ImaAdsLoader */ public ImaAdsLoader buildForAdTag(Uri adTagUri) { return new ImaAdsLoader( - context, - adTagUri, - imaSdkSettings, - /* adsResponse= */ null, - adPreloadTimeoutMs, - vastLoadTimeoutMs, - mediaLoadTimeoutMs, - mediaBitrate, - focusSkipButtonWhenAvailable, - playAdBeforeStartPosition, - adUiElements, - companionAdSlots, - adErrorListener, - adEventListener, - imaFactory); + /* builder= */ this, /* adTagUri= */ adTagUri, /* adsResponse= */ null); } /** @@ -343,22 +329,7 @@ public final class ImaAdsLoader * @return The new {@link ImaAdsLoader}. */ public ImaAdsLoader buildForAdsResponse(String adsResponse) { - return new ImaAdsLoader( - context, - /* adTagUri= */ null, - imaSdkSettings, - adsResponse, - adPreloadTimeoutMs, - vastLoadTimeoutMs, - mediaLoadTimeoutMs, - mediaBitrate, - focusSkipButtonWhenAvailable, - playAdBeforeStartPosition, - adUiElements, - companionAdSlots, - adErrorListener, - adEventListener, - imaFactory); + return new ImaAdsLoader(/* builder= */ this, /* adTagUri= */ null, adsResponse); } } @@ -520,56 +491,27 @@ public final class ImaAdsLoader * more information. */ public ImaAdsLoader(Context context, Uri adTagUri) { - this( - context, - adTagUri, - /* imaSdkSettings= */ null, - /* adsResponse= */ null, - /* adPreloadTimeoutMs= */ Builder.DEFAULT_AD_PRELOAD_TIMEOUT_MS, - /* vastLoadTimeoutMs= */ TIMEOUT_UNSET, - /* mediaLoadTimeoutMs= */ TIMEOUT_UNSET, - /* mediaBitrate= */ BITRATE_UNSET, - /* focusSkipButtonWhenAvailable= */ true, - /* playAdBeforeStartPosition= */ true, - /* adUiElements= */ null, - /* companionAdSlots= */ null, - /* adErrorListener= */ null, - /* adEventListener= */ null, - /* imaFactory= */ new DefaultImaFactory()); + this(new Builder(context), adTagUri, /* adsResponse= */ null); } @SuppressWarnings({"nullness:argument.type.incompatible", "methodref.receiver.bound.invalid"}) - private ImaAdsLoader( - Context context, - @Nullable Uri adTagUri, - @Nullable ImaSdkSettings imaSdkSettings, - @Nullable String adsResponse, - long adPreloadTimeoutMs, - int vastLoadTimeoutMs, - int mediaLoadTimeoutMs, - int mediaBitrate, - boolean focusSkipButtonWhenAvailable, - boolean playAdBeforeStartPosition, - @Nullable Set adUiElements, - @Nullable Collection companionAdSlots, - @Nullable AdErrorListener adErrorListener, - @Nullable AdEventListener adEventListener, - ImaFactory imaFactory) { + private ImaAdsLoader(Builder builder, @Nullable Uri adTagUri, @Nullable String adsResponse) { checkArgument(adTagUri != null || adsResponse != null); - this.context = context.getApplicationContext(); + this.context = builder.context.getApplicationContext(); this.adTagUri = adTagUri; this.adsResponse = adsResponse; - this.adPreloadTimeoutMs = adPreloadTimeoutMs; - this.vastLoadTimeoutMs = vastLoadTimeoutMs; - this.mediaLoadTimeoutMs = mediaLoadTimeoutMs; - this.mediaBitrate = mediaBitrate; - this.focusSkipButtonWhenAvailable = focusSkipButtonWhenAvailable; - this.playAdBeforeStartPosition = playAdBeforeStartPosition; - this.adUiElements = adUiElements; - this.companionAdSlots = companionAdSlots; - this.adErrorListener = adErrorListener; - this.adEventListener = adEventListener; - this.imaFactory = imaFactory; + this.adPreloadTimeoutMs = builder.adPreloadTimeoutMs; + this.vastLoadTimeoutMs = builder.vastLoadTimeoutMs; + this.mediaLoadTimeoutMs = builder.mediaLoadTimeoutMs; + this.mediaBitrate = builder.mediaBitrate; + this.focusSkipButtonWhenAvailable = builder.focusSkipButtonWhenAvailable; + this.playAdBeforeStartPosition = builder.playAdBeforeStartPosition; + this.adUiElements = builder.adUiElements; + this.companionAdSlots = builder.companionAdSlots; + this.adErrorListener = builder.adErrorListener; + this.adEventListener = builder.adEventListener; + this.imaFactory = builder.imaFactory; + @Nullable ImaSdkSettings imaSdkSettings = builder.imaSdkSettings; if (imaSdkSettings == null) { imaSdkSettings = imaFactory.createImaSdkSettings(); if (DEBUG) { From f3767b3185202338725274a37b4ce0c3080ac417 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Wed, 30 Sep 2020 11:54:01 +0100 Subject: [PATCH 072/110] Add ImaUtil for IMA extension utilities PiperOrigin-RevId: 334567234 --- .../ext/ima/AdPlaybackStateFactory.java | 56 -------- .../exoplayer2/ext/ima/ImaAdsLoader.java | 77 ++--------- .../android/exoplayer2/ext/ima/ImaUtil.java | 128 ++++++++++++++++++ .../exoplayer2/ext/ima/ImaAdsLoaderTest.java | 28 ++-- 4 files changed, 155 insertions(+), 134 deletions(-) delete mode 100644 extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/AdPlaybackStateFactory.java create mode 100644 extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaUtil.java diff --git a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/AdPlaybackStateFactory.java b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/AdPlaybackStateFactory.java deleted file mode 100644 index a97307a419..0000000000 --- a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/AdPlaybackStateFactory.java +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Copyright (C) 2020 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.google.android.exoplayer2.ext.ima; - -import com.google.android.exoplayer2.C; -import com.google.android.exoplayer2.source.ads.AdPlaybackState; -import java.util.Arrays; -import java.util.List; - -/** - * Static utility class for constructing {@link AdPlaybackState} instances from IMA-specific data. - */ -/* package */ final class AdPlaybackStateFactory { - private AdPlaybackStateFactory() {} - - /** - * Construct an {@link AdPlaybackState} from the provided {@code cuePoints}. - * - * @param cuePoints The cue points of the ads in seconds. - * @return The {@link AdPlaybackState}. - */ - public static AdPlaybackState fromCuePoints(List cuePoints) { - if (cuePoints.isEmpty()) { - // If no cue points are specified, there is a preroll ad. - return new AdPlaybackState(/* adGroupTimesUs...= */ 0); - } - - int count = cuePoints.size(); - long[] adGroupTimesUs = new long[count]; - int adGroupIndex = 0; - for (int i = 0; i < count; i++) { - double cuePoint = cuePoints.get(i); - if (cuePoint == -1.0) { - adGroupTimesUs[count - 1] = C.TIME_END_OF_SOURCE; - } else { - adGroupTimesUs[adGroupIndex++] = Math.round(C.MICROS_PER_SECOND * cuePoint); - } - } - // Cue points may be out of order, so sort them. - Arrays.sort(adGroupTimesUs, 0, adGroupIndex); - return new AdPlaybackState(adGroupTimesUs); - } -} diff --git a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java index 157fab938c..592920bfc4 100644 --- a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java +++ b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java @@ -33,7 +33,6 @@ import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; import com.google.ads.interactivemedia.v3.api.AdDisplayContainer; import com.google.ads.interactivemedia.v3.api.AdError; -import com.google.ads.interactivemedia.v3.api.AdError.AdErrorCode; import com.google.ads.interactivemedia.v3.api.AdErrorEvent; import com.google.ads.interactivemedia.v3.api.AdErrorEvent.AdErrorListener; import com.google.ads.interactivemedia.v3.api.AdEvent; @@ -134,7 +133,7 @@ public final class ImaAdsLoader private int mediaBitrate; private boolean focusSkipButtonWhenAvailable; private boolean playAdBeforeStartPosition; - private ImaFactory imaFactory; + private ImaUtil.ImaFactory imaFactory; /** * Creates a new builder for {@link ImaAdsLoader}. @@ -303,7 +302,7 @@ public final class ImaAdsLoader } @VisibleForTesting - /* package */ Builder setImaFactory(ImaFactory imaFactory) { + /* package */ Builder setImaFactory(ImaUtil.ImaFactory imaFactory) { this.imaFactory = checkNotNull(imaFactory); return this; } @@ -397,7 +396,7 @@ public final class ImaAdsLoader @Nullable private final Collection companionAdSlots; @Nullable private final AdErrorListener adErrorListener; @Nullable private final AdEventListener adEventListener; - private final ImaFactory imaFactory; + private final ImaUtil.ImaFactory imaFactory; private final ImaSdkSettings imaSdkSettings; private final Timeline.Period period; private final Handler handler; @@ -677,7 +676,7 @@ public final class ImaAdsLoader adsManager.resume(); } } else if (adsManager != null) { - adPlaybackState = AdPlaybackStateFactory.fromCuePoints(adsManager.getAdCuePoints()); + adPlaybackState = ImaUtil.getInitialAdPlaybackStateForCuePoints(adsManager.getAdCuePoints()); updateAdPlaybackState(); } else { // Ads haven't loaded yet, so request them. @@ -688,7 +687,7 @@ public final class ImaAdsLoader adDisplayContainer.registerFriendlyObstruction( imaFactory.createFriendlyObstruction( overlayInfo.view, - getFriendlyObstructionPurpose(overlayInfo.purpose), + ImaUtil.getFriendlyObstructionPurpose(overlayInfo.purpose), overlayInfo.reasonDetail)); } } @@ -1481,21 +1480,6 @@ public final class ImaAdsLoader return "AdMediaInfo[" + adMediaInfo.getUrl() + (adInfo != null ? ", " + adInfo : "") + "]"; } - private static FriendlyObstructionPurpose getFriendlyObstructionPurpose( - @OverlayInfo.Purpose int purpose) { - switch (purpose) { - case OverlayInfo.PURPOSE_CONTROLS: - return FriendlyObstructionPurpose.VIDEO_CONTROLS; - case OverlayInfo.PURPOSE_CLOSE_AD: - return FriendlyObstructionPurpose.CLOSE_AD; - case OverlayInfo.PURPOSE_NOT_VISIBLE: - return FriendlyObstructionPurpose.NOT_VISIBLE; - case OverlayInfo.PURPOSE_OTHER: - default: - return FriendlyObstructionPurpose.OTHER; - } - } - private static DataSpec getAdsDataSpec(@Nullable Uri adTagUri) { return new DataSpec(adTagUri != null ? adTagUri : Uri.EMPTY); } @@ -1509,13 +1493,6 @@ public final class ImaAdsLoader : timeline.getPeriod(/* periodIndex= */ 0, period).getPositionInWindowMs()); } - private static boolean isAdGroupLoadError(AdError adError) { - // TODO: Find out what other errors need to be handled (if any), and whether each one relates to - // a single ad, ad group or the whole timeline. - return adError.getErrorCode() == AdErrorCode.VAST_LINEAR_ASSET_MISMATCH - || adError.getErrorCode() == AdErrorCode.UNKNOWN_ERROR; - } - private static Looper getImaLooper() { // IMA SDK callbacks occur on the main thread. This method can be used to check that the player // is using the same looper, to ensure all interaction with this class is on the main thread. @@ -1549,38 +1526,6 @@ public final class ImaAdsLoader } } - /** Factory for objects provided by the IMA SDK. */ - @VisibleForTesting - /* package */ interface ImaFactory { - /** Creates {@link ImaSdkSettings} for configuring the IMA SDK. */ - ImaSdkSettings createImaSdkSettings(); - /** - * Creates {@link AdsRenderingSettings} for giving the {@link AdsManager} parameters that - * control rendering of ads. - */ - AdsRenderingSettings createAdsRenderingSettings(); - /** - * Creates an {@link AdDisplayContainer} to hold the player for video ads, a container for - * non-linear ads, and slots for companion ads. - */ - AdDisplayContainer createAdDisplayContainer(ViewGroup container, VideoAdPlayer player); - /** Creates an {@link AdDisplayContainer} to hold the player for audio ads. */ - AdDisplayContainer createAudioAdDisplayContainer(Context context, VideoAdPlayer player); - /** - * Creates a {@link FriendlyObstruction} to describe an obstruction considered "friendly" for - * viewability measurement purposes. - */ - FriendlyObstruction createFriendlyObstruction( - View view, - FriendlyObstructionPurpose friendlyObstructionPurpose, - @Nullable String reasonDetail); - /** Creates an {@link AdsRequest} to contain the data used to request ads. */ - AdsRequest createAdsRequest(); - /** Creates an {@link AdsLoader} for requesting ads using the specified settings. */ - AdsLoader createAdsLoader( - Context context, ImaSdkSettings imaSdkSettings, AdDisplayContainer adDisplayContainer); - } - private final class ComponentListener implements AdsLoadedListener, ContentProgressProvider, @@ -1610,7 +1555,8 @@ public final class ImaAdsLoader if (player != null) { // If a player is attached already, start playback immediately. try { - adPlaybackState = AdPlaybackStateFactory.fromCuePoints(adsManager.getAdCuePoints()); + adPlaybackState = + ImaUtil.getInitialAdPlaybackStateForCuePoints(adsManager.getAdCuePoints()); hasAdPlaybackState = true; updateAdPlaybackState(); } catch (RuntimeException e) { @@ -1680,7 +1626,7 @@ public final class ImaAdsLoader adPlaybackState = AdPlaybackState.NONE; hasAdPlaybackState = true; updateAdPlaybackState(); - } else if (isAdGroupLoadError(error)) { + } else if (ImaUtil.isAdGroupLoadError(error)) { try { handleAdGroupLoadError(error); } catch (RuntimeException e) { @@ -1795,8 +1741,11 @@ public final class ImaAdsLoader } } - /** Default {@link ImaFactory} for non-test usage, which delegates to {@link ImaSdkFactory}. */ - private static final class DefaultImaFactory implements ImaFactory { + /** + * Default {@link ImaUtil.ImaFactory} for non-test usage, which delegates to {@link + * ImaSdkFactory}. + */ + private static final class DefaultImaFactory implements ImaUtil.ImaFactory { @Override public ImaSdkSettings createImaSdkSettings() { return ImaSdkFactory.getInstance().createImaSdkSettings(); diff --git a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaUtil.java b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaUtil.java new file mode 100644 index 0000000000..c4b2c3dca3 --- /dev/null +++ b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaUtil.java @@ -0,0 +1,128 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.ext.ima; + +import android.content.Context; +import android.view.View; +import android.view.ViewGroup; +import androidx.annotation.Nullable; +import com.google.ads.interactivemedia.v3.api.AdDisplayContainer; +import com.google.ads.interactivemedia.v3.api.AdError; +import com.google.ads.interactivemedia.v3.api.AdsLoader; +import com.google.ads.interactivemedia.v3.api.AdsManager; +import com.google.ads.interactivemedia.v3.api.AdsRenderingSettings; +import com.google.ads.interactivemedia.v3.api.AdsRequest; +import com.google.ads.interactivemedia.v3.api.FriendlyObstruction; +import com.google.ads.interactivemedia.v3.api.FriendlyObstructionPurpose; +import com.google.ads.interactivemedia.v3.api.ImaSdkSettings; +import com.google.ads.interactivemedia.v3.api.player.VideoAdPlayer; +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.source.ads.AdPlaybackState; +import com.google.android.exoplayer2.source.ads.AdsLoader.OverlayInfo; +import java.util.Arrays; +import java.util.List; + +/** Utilities for working with IMA SDK and IMA extension data types. */ +/* package */ final class ImaUtil { + + /** Factory for objects provided by the IMA SDK. */ + public interface ImaFactory { + /** Creates {@link ImaSdkSettings} for configuring the IMA SDK. */ + ImaSdkSettings createImaSdkSettings(); + /** + * Creates {@link AdsRenderingSettings} for giving the {@link AdsManager} parameters that + * control rendering of ads. + */ + AdsRenderingSettings createAdsRenderingSettings(); + /** + * Creates an {@link AdDisplayContainer} to hold the player for video ads, a container for + * non-linear ads, and slots for companion ads. + */ + AdDisplayContainer createAdDisplayContainer(ViewGroup container, VideoAdPlayer player); + /** Creates an {@link AdDisplayContainer} to hold the player for audio ads. */ + AdDisplayContainer createAudioAdDisplayContainer(Context context, VideoAdPlayer player); + /** + * Creates a {@link FriendlyObstruction} to describe an obstruction considered "friendly" for + * viewability measurement purposes. + */ + FriendlyObstruction createFriendlyObstruction( + View view, + FriendlyObstructionPurpose friendlyObstructionPurpose, + @Nullable String reasonDetail); + /** Creates an {@link AdsRequest} to contain the data used to request ads. */ + AdsRequest createAdsRequest(); + /** Creates an {@link AdsLoader} for requesting ads using the specified settings. */ + AdsLoader createAdsLoader( + Context context, ImaSdkSettings imaSdkSettings, AdDisplayContainer adDisplayContainer); + } + + /** + * Returns the IMA {@link FriendlyObstructionPurpose} corresponding to the given {@link + * OverlayInfo#purpose}. + */ + public static FriendlyObstructionPurpose getFriendlyObstructionPurpose( + @OverlayInfo.Purpose int purpose) { + switch (purpose) { + case OverlayInfo.PURPOSE_CONTROLS: + return FriendlyObstructionPurpose.VIDEO_CONTROLS; + case OverlayInfo.PURPOSE_CLOSE_AD: + return FriendlyObstructionPurpose.CLOSE_AD; + case OverlayInfo.PURPOSE_NOT_VISIBLE: + return FriendlyObstructionPurpose.NOT_VISIBLE; + case OverlayInfo.PURPOSE_OTHER: + default: + return FriendlyObstructionPurpose.OTHER; + } + } + + /** + * Returns an initial {@link AdPlaybackState} with ad groups at the provided {@code cuePoints}. + * + * @param cuePoints The cue points of the ads in seconds. + * @return The {@link AdPlaybackState}. + */ + public static AdPlaybackState getInitialAdPlaybackStateForCuePoints(List cuePoints) { + if (cuePoints.isEmpty()) { + // If no cue points are specified, there is a preroll ad. + return new AdPlaybackState(/* adGroupTimesUs...= */ 0); + } + + int count = cuePoints.size(); + long[] adGroupTimesUs = new long[count]; + int adGroupIndex = 0; + for (int i = 0; i < count; i++) { + double cuePoint = cuePoints.get(i); + if (cuePoint == -1.0) { + adGroupTimesUs[count - 1] = C.TIME_END_OF_SOURCE; + } else { + adGroupTimesUs[adGroupIndex++] = Math.round(C.MICROS_PER_SECOND * cuePoint); + } + } + // Cue points may be out of order, so sort them. + Arrays.sort(adGroupTimesUs, 0, adGroupIndex); + return new AdPlaybackState(adGroupTimesUs); + } + + /** Returns whether the ad error indicates that an entire ad group failed to load. */ + public static boolean isAdGroupLoadError(AdError adError) { + // TODO: Find out what other errors need to be handled (if any), and whether each one relates to + // a single ad, ad group or the whole timeline. + return adError.getErrorCode() == AdError.AdErrorCode.VAST_LINEAR_ASSET_MISMATCH + || adError.getErrorCode() == AdError.AdErrorCode.UNKNOWN_ERROR; + } + + private ImaUtil() {} +} diff --git a/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoaderTest.java b/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoaderTest.java index c2cc384888..9861065454 100644 --- a/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoaderTest.java +++ b/extensions/ima/src/test/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoaderTest.java @@ -55,7 +55,7 @@ import com.google.android.exoplayer2.MediaItem; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.Timeline.Period; -import com.google.android.exoplayer2.ext.ima.ImaAdsLoader.ImaFactory; +import com.google.android.exoplayer2.ext.ima.ImaUtil.ImaFactory; import com.google.android.exoplayer2.source.MaskingMediaSource.PlaceholderTimeline; import com.google.android.exoplayer2.source.ads.AdPlaybackState; import com.google.android.exoplayer2.source.ads.AdsLoader; @@ -378,7 +378,7 @@ public final class ImaAdsLoaderTest { assertThat(adsLoaderListener.adPlaybackState) .isEqualTo( - AdPlaybackStateFactory.fromCuePoints(cuePoints) + ImaUtil.getInitialAdPlaybackStateForCuePoints(cuePoints) .withContentDurationUs(CONTENT_PERIOD_DURATION_US)); } @@ -402,7 +402,7 @@ public final class ImaAdsLoaderTest { assertThat(adsLoaderListener.adPlaybackState) .isEqualTo( - AdPlaybackStateFactory.fromCuePoints(cuePoints) + ImaUtil.getInitialAdPlaybackStateForCuePoints(cuePoints) .withContentDurationUs(CONTENT_PERIOD_DURATION_US) .withAdDurationsUs(new long[][] {{TEST_AD_DURATION_US}}) .withAdCount(/* adGroupIndex= */ 0, /* adCount= */ 1) @@ -424,7 +424,7 @@ public final class ImaAdsLoaderTest { verify(mockAdsRenderingSettings, never()).setPlayAdsAfterTime(anyDouble()); assertThat(adsLoaderListener.adPlaybackState) .isEqualTo( - AdPlaybackStateFactory.fromCuePoints(cuePoints) + ImaUtil.getInitialAdPlaybackStateForCuePoints(cuePoints) .withContentDurationUs(CONTENT_PERIOD_DURATION_US)); } @@ -448,7 +448,7 @@ public final class ImaAdsLoaderTest { .of(expectedPlayAdsAfterTimeUs / C.MICROS_PER_SECOND); assertThat(adsLoaderListener.adPlaybackState) .isEqualTo( - AdPlaybackStateFactory.fromCuePoints(cuePoints) + ImaUtil.getInitialAdPlaybackStateForCuePoints(cuePoints) .withContentDurationUs(CONTENT_PERIOD_DURATION_US) .withSkippedAdGroup(/* adGroupIndex= */ 0)); } @@ -473,7 +473,7 @@ public final class ImaAdsLoaderTest { .of(expectedPlayAdsAfterTimeUs / C.MICROS_PER_SECOND); assertThat(adsLoaderListener.adPlaybackState) .isEqualTo( - AdPlaybackStateFactory.fromCuePoints(cuePoints) + ImaUtil.getInitialAdPlaybackStateForCuePoints(cuePoints) .withContentDurationUs(CONTENT_PERIOD_DURATION_US) .withSkippedAdGroup(/* adGroupIndex= */ 0)); } @@ -500,7 +500,7 @@ public final class ImaAdsLoaderTest { verify(mockAdsRenderingSettings, never()).setPlayAdsAfterTime(anyDouble()); assertThat(adsLoaderListener.adPlaybackState) .isEqualTo( - AdPlaybackStateFactory.fromCuePoints(cuePoints) + ImaUtil.getInitialAdPlaybackStateForCuePoints(cuePoints) .withContentDurationUs(CONTENT_PERIOD_DURATION_US)); } @@ -531,7 +531,7 @@ public final class ImaAdsLoaderTest { .of(expectedPlayAdsAfterTimeUs / C.MICROS_PER_SECOND); assertThat(adsLoaderListener.adPlaybackState) .isEqualTo( - AdPlaybackStateFactory.fromCuePoints(cuePoints) + ImaUtil.getInitialAdPlaybackStateForCuePoints(cuePoints) .withContentDurationUs(CONTENT_PERIOD_DURATION_US) .withSkippedAdGroup(/* adGroupIndex= */ 0)); } @@ -563,7 +563,7 @@ public final class ImaAdsLoaderTest { .of(expectedPlayAdsAfterTimeUs / C.MICROS_PER_SECOND); assertThat(adsLoaderListener.adPlaybackState) .isEqualTo( - AdPlaybackStateFactory.fromCuePoints(cuePoints) + ImaUtil.getInitialAdPlaybackStateForCuePoints(cuePoints) .withSkippedAdGroup(/* adGroupIndex= */ 0) .withContentDurationUs(CONTENT_PERIOD_DURATION_US)); } @@ -595,7 +595,7 @@ public final class ImaAdsLoaderTest { .of(expectedPlayAdsAfterTimeUs / C.MICROS_PER_SECOND); assertThat(adsLoaderListener.adPlaybackState) .isEqualTo( - AdPlaybackStateFactory.fromCuePoints(cuePoints) + ImaUtil.getInitialAdPlaybackStateForCuePoints(cuePoints) .withContentDurationUs(CONTENT_PERIOD_DURATION_US) .withSkippedAdGroup(/* adGroupIndex= */ 0)); } @@ -622,7 +622,7 @@ public final class ImaAdsLoaderTest { verify(mockAdsManager).destroy(); assertThat(adsLoaderListener.adPlaybackState) .isEqualTo( - AdPlaybackStateFactory.fromCuePoints(cuePoints) + ImaUtil.getInitialAdPlaybackStateForCuePoints(cuePoints) .withContentDurationUs(CONTENT_PERIOD_DURATION_US) .withSkippedAdGroup(/* adGroupIndex= */ 0) .withSkippedAdGroup(/* adGroupIndex= */ 1)); @@ -663,7 +663,7 @@ public final class ImaAdsLoaderTest { .of(expectedPlayAdsAfterTimeUs / C.MICROS_PER_SECOND); assertThat(adsLoaderListener.adPlaybackState) .isEqualTo( - AdPlaybackStateFactory.fromCuePoints(cuePoints) + ImaUtil.getInitialAdPlaybackStateForCuePoints(cuePoints) .withSkippedAdGroup(/* adGroupIndex= */ 0) .withContentDurationUs(CONTENT_PERIOD_DURATION_US)); } @@ -702,7 +702,7 @@ public final class ImaAdsLoaderTest { .of(expectedPlayAdsAfterTimeUs / C.MICROS_PER_SECOND); assertThat(adsLoaderListener.adPlaybackState) .isEqualTo( - AdPlaybackStateFactory.fromCuePoints(cuePoints) + ImaUtil.getInitialAdPlaybackStateForCuePoints(cuePoints) .withContentDurationUs(CONTENT_PERIOD_DURATION_US) .withSkippedAdGroup(/* adGroupIndex= */ 0)); } @@ -761,7 +761,7 @@ public final class ImaAdsLoaderTest { assertThat(adsLoaderListener.adPlaybackState) .isEqualTo( - AdPlaybackStateFactory.fromCuePoints(cuePoints) + ImaUtil.getInitialAdPlaybackStateForCuePoints(cuePoints) .withContentDurationUs(CONTENT_PERIOD_DURATION_US) .withAdCount(/* adGroupIndex= */ 0, /* adCount= */ 1) .withAdUri(/* adGroupIndex= */ 0, /* adIndexInAdGroup= */ 0, TEST_URI) From bd312ec906e983a9c444d7aa5e1a3b3f26e2bcd4 Mon Sep 17 00:00:00 2001 From: ibaker Date: Wed, 30 Sep 2020 16:15:31 +0100 Subject: [PATCH 073/110] Preserve limit when resetting ParsableByteArray in OggPacket#populate When I moved ParsableByteArray#data behind a getter I replaced some assignments with calls to reset(byte[]): https://github.com/google/ExoPlayer/commit/ce2e6e2fd625db787b1f400614adcd7458144bbd reset(byte[]) deliberately sets `limit` to `data.length`, in order to handle cases that were reassigning `data` but not updating `limit`. However OggPacket was already using `limit` to track where to write 'new' data into the array, so changing `limit` to `data.length` caused us to try and write new data beyond the end of the array. I looked at other uses of reset(byte[]) in https://github.com/google/ExoPlayer/commit/ce2e6e2fd625db787b1f400614adcd7458144bbd and condluded the only other usage in MatroskaExtractor is legit and shouldn't be updated like this (because MatroskaExtractor previously *wasn't* correctly updating/maintaining `limit`). Issue: #7992 PiperOrigin-RevId: 334601586 --- RELEASENOTES.md | 2 + .../exoplayer2/extractor/ogg/OggPacket.java | 7 +- .../ogg/OggExtractorParameterizedTest.java | 20 +- ...bear_vorbis_with_large_metadata.ogg.0.dump | 740 ++++++++++++++++++ ...bear_vorbis_with_large_metadata.ogg.1.dump | 456 +++++++++++ ...bear_vorbis_with_large_metadata.ogg.2.dump | 216 +++++ ...bear_vorbis_with_large_metadata.ogg.3.dump | 20 + ...ith_large_metadata.ogg.unknown_length.dump | 737 +++++++++++++++++ .../ogg/bear_vorbis_with_large_metadata.ogg | Bin 0 -> 111383 bytes 9 files changed, 2194 insertions(+), 4 deletions(-) create mode 100644 testdata/src/test/assets/extractordumps/ogg/bear_vorbis_with_large_metadata.ogg.0.dump create mode 100644 testdata/src/test/assets/extractordumps/ogg/bear_vorbis_with_large_metadata.ogg.1.dump create mode 100644 testdata/src/test/assets/extractordumps/ogg/bear_vorbis_with_large_metadata.ogg.2.dump create mode 100644 testdata/src/test/assets/extractordumps/ogg/bear_vorbis_with_large_metadata.ogg.3.dump create mode 100644 testdata/src/test/assets/extractordumps/ogg/bear_vorbis_with_large_metadata.ogg.unknown_length.dump create mode 100644 testdata/src/test/assets/media/ogg/bear_vorbis_with_large_metadata.ogg diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 55aaa5d486..3404a3e7d9 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -23,6 +23,8 @@ ([#7967](https://github.com/google/ExoPlayer/issues/7967)). * Use TLEN ID3 tag to compute the duration in Mp3Extractor ([#7949](https://github.com/google/ExoPlayer/issues/7949)). + * Fix regression for Ogg files with packets that span multiple pages + ([#7992](https://github.com/google/ExoPlayer/issues/7992)). * UI * Add the option to sort tracks by `Format` in `TrackSelectionView` and `TrackSelectionDialogBuilder` diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ogg/OggPacket.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ogg/OggPacket.java index 450bff4a36..c7718e7fa9 100644 --- a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ogg/OggPacket.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ogg/OggPacket.java @@ -88,7 +88,9 @@ import java.util.Arrays; int segmentIndex = currentSegmentIndex + segmentCount; if (size > 0) { if (packetArray.capacity() < packetArray.limit() + size) { - packetArray.reset(Arrays.copyOf(packetArray.getData(), packetArray.limit() + size)); + packetArray.reset( + Arrays.copyOf(packetArray.getData(), packetArray.limit() + size), + /* limit= */ packetArray.limit()); } input.readFully(packetArray.getData(), packetArray.limit(), size); packetArray.setLimit(packetArray.limit() + size); @@ -131,7 +133,8 @@ import java.util.Arrays; } packetArray.reset( Arrays.copyOf( - packetArray.getData(), max(OggPageHeader.MAX_PAGE_PAYLOAD, packetArray.limit()))); + packetArray.getData(), max(OggPageHeader.MAX_PAGE_PAYLOAD, packetArray.limit())), + /* limit= */ packetArray.limit()); } /** diff --git a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/ogg/OggExtractorParameterizedTest.java b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/ogg/OggExtractorParameterizedTest.java index cc78d59bf4..0731cfd95e 100644 --- a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/ogg/OggExtractorParameterizedTest.java +++ b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/ogg/OggExtractorParameterizedTest.java @@ -60,11 +60,27 @@ public final class OggExtractorParameterizedTest { OggExtractor::new, "media/ogg/bear_vorbis.ogg", simulationConfig); } - // Ensure the extractor can handle non-contiguous pages by using a file with 10 bytes of garbage - // data before the start of the second page. + /** + * Ensure the extractor can handle non-contiguous pages by using a file with 10 bytes of garbage + * data before the start of the second page. + * + *

    https://github.com/google/ExoPlayer/issues/7230 + */ @Test public void vorbisWithGapBeforeSecondPage() throws Exception { ExtractorAsserts.assertBehavior( OggExtractor::new, "media/ogg/bear_vorbis_gap.ogg", simulationConfig); } + + /** + * Use some very large Vorbis Comment metadata to create a packet that is larger than a single Ogg + * page. + * + *

    https://github.com/google/ExoPlayer/issues/7992 + */ + @Test + public void vorbisWithPacketSpanningBetweenPages() throws Exception { + ExtractorAsserts.assertBehavior( + OggExtractor::new, "media/ogg/bear_vorbis_with_large_metadata.ogg", simulationConfig); + } } diff --git a/testdata/src/test/assets/extractordumps/ogg/bear_vorbis_with_large_metadata.ogg.0.dump b/testdata/src/test/assets/extractordumps/ogg/bear_vorbis_with_large_metadata.ogg.0.dump new file mode 100644 index 0000000000..92aec373b5 --- /dev/null +++ b/testdata/src/test/assets/extractordumps/ogg/bear_vorbis_with_large_metadata.ogg.0.dump @@ -0,0 +1,740 @@ +seekMap: + isSeekable = true + duration = 2741000 + getPosition(0) = [[timeUs=0, position=84969]] + getPosition(1) = [[timeUs=1, position=84969]] + getPosition(1370500) = [[timeUs=1370500, position=84969]] + getPosition(2741000) = [[timeUs=2741000, position=84969]] +numberOfTracks = 1 +track 0: + total output bytes = 26873 + sample count = 180 + format 0: + averageBitrate = 112000 + sampleMimeType = audio/vorbis + channelCount = 2 + sampleRate = 48000 + initializationData: + data = length 30, hash 9A8FF207 + data = length 3832, hash 8A406249 + sample 0: + time = 0 + flags = 1 + data = length 49, hash 2FFF94F0 + sample 1: + time = 0 + flags = 1 + data = length 44, hash 3946418A + sample 2: + time = 2666 + flags = 1 + data = length 55, hash 2A0B878E + sample 3: + time = 5333 + flags = 1 + data = length 53, hash CC3B6879 + sample 4: + time = 8000 + flags = 1 + data = length 215, hash 106AE950 + sample 5: + time = 20000 + flags = 1 + data = length 192, hash 2B219F53 + sample 6: + time = 41333 + flags = 1 + data = length 197, hash FBC39422 + sample 7: + time = 62666 + flags = 1 + data = length 209, hash 386E8979 + sample 8: + time = 84000 + flags = 1 + data = length 42, hash E81162C1 + sample 9: + time = 96000 + flags = 1 + data = length 41, hash F15BEE36 + sample 10: + time = 98666 + flags = 1 + data = length 42, hash D67EB19 + sample 11: + time = 101333 + flags = 1 + data = length 42, hash F4DE4792 + sample 12: + time = 104000 + flags = 1 + data = length 53, hash 80F66AC3 + sample 13: + time = 106666 + flags = 1 + data = length 56, hash DCB9DFC4 + sample 14: + time = 109333 + flags = 1 + data = length 55, hash 4E0C4E9D + sample 15: + time = 112000 + flags = 1 + data = length 203, hash 176B6862 + sample 16: + time = 124000 + flags = 1 + data = length 193, hash AB13CB10 + sample 17: + time = 145333 + flags = 1 + data = length 203, hash DE63DE9F + sample 18: + time = 166666 + flags = 1 + data = length 194, hash 4A9508A2 + sample 19: + time = 188000 + flags = 1 + data = length 210, hash 196899B3 + sample 20: + time = 209333 + flags = 1 + data = length 195, hash B68407F1 + sample 21: + time = 230666 + flags = 1 + data = length 193, hash A1FA86E3 + sample 22: + time = 252000 + flags = 1 + data = length 194, hash 5C0B9343 + sample 23: + time = 273333 + flags = 1 + data = length 198, hash 789914B2 + sample 24: + time = 294666 + flags = 1 + data = length 183, hash 1B82D11F + sample 25: + time = 316000 + flags = 1 + data = length 199, hash D5B848F4 + sample 26: + time = 337333 + flags = 1 + data = length 192, hash B34427EA + sample 27: + time = 358666 + flags = 1 + data = length 199, hash C2599BB5 + sample 28: + time = 380000 + flags = 1 + data = length 195, hash BFD83194 + sample 29: + time = 401333 + flags = 1 + data = length 199, hash C9A7F7CA + sample 30: + time = 422666 + flags = 1 + data = length 44, hash 5D76EAD6 + sample 31: + time = 434666 + flags = 1 + data = length 43, hash 8619C423 + sample 32: + time = 437333 + flags = 1 + data = length 43, hash E490BBE + sample 33: + time = 440000 + flags = 1 + data = length 53, hash 8A557CAE + sample 34: + time = 442666 + flags = 1 + data = length 56, hash 81007BBA + sample 35: + time = 445333 + flags = 1 + data = length 56, hash 4E4DD67F + sample 36: + time = 448000 + flags = 1 + data = length 222, hash 414188AB + sample 37: + time = 460000 + flags = 1 + data = length 202, hash 67A07D30 + sample 38: + time = 481333 + flags = 1 + data = length 200, hash E357D853 + sample 39: + time = 502666 + flags = 1 + data = length 203, hash 4653DC90 + sample 40: + time = 524000 + flags = 1 + data = length 192, hash A65E6C09 + sample 41: + time = 545333 + flags = 1 + data = length 202, hash FBEAC508 + sample 42: + time = 566666 + flags = 1 + data = length 202, hash E9B7B59F + sample 43: + time = 588000 + flags = 1 + data = length 204, hash E24AA78E + sample 44: + time = 609333 + flags = 1 + data = length 41, hash 3FBC5216 + sample 45: + time = 621333 + flags = 1 + data = length 47, hash 153FBC55 + sample 46: + time = 624000 + flags = 1 + data = length 42, hash 2B493D6C + sample 47: + time = 626666 + flags = 1 + data = length 42, hash 8303BEE3 + sample 48: + time = 629333 + flags = 1 + data = length 62, hash 71AEE50B + sample 49: + time = 632000 + flags = 1 + data = length 54, hash 52F61908 + sample 50: + time = 634666 + flags = 1 + data = length 45, hash 7BD3E3A1 + sample 51: + time = 637333 + flags = 1 + data = length 41, hash E0F65472 + sample 52: + time = 640000 + flags = 1 + data = length 45, hash 41838675 + sample 53: + time = 642666 + flags = 1 + data = length 44, hash FCBC2147 + sample 54: + time = 645333 + flags = 1 + data = length 45, hash 1A5987E3 + sample 55: + time = 648000 + flags = 1 + data = length 43, hash 99074864 + sample 56: + time = 650666 + flags = 1 + data = length 57, hash D4A9B60A + sample 57: + time = 653333 + flags = 1 + data = length 52, hash 302129DA + sample 58: + time = 656000 + flags = 1 + data = length 57, hash D8DD99C0 + sample 59: + time = 658666 + flags = 1 + data = length 206, hash F4B9EF26 + sample 60: + time = 670666 + flags = 1 + data = length 197, hash 7B8ACC8A + sample 61: + time = 692000 + flags = 1 + data = length 186, hash 161027CB + sample 62: + time = 713333 + flags = 1 + data = length 186, hash 1D6871B6 + sample 63: + time = 734666 + flags = 1 + data = length 201, hash 536E9FDB + sample 64: + time = 756000 + flags = 1 + data = length 192, hash D38EFAC5 + sample 65: + time = 777333 + flags = 1 + data = length 194, hash 4B394EF3 + sample 66: + time = 798666 + flags = 1 + data = length 206, hash 1B31BA99 + sample 67: + time = 820000 + flags = 1 + data = length 212, hash AD061F43 + sample 68: + time = 841333 + flags = 1 + data = length 180, hash 6D1F7481 + sample 69: + time = 862666 + flags = 1 + data = length 195, hash D80B21F + sample 70: + time = 884000 + flags = 1 + data = length 186, hash D367882 + sample 71: + time = 905333 + flags = 1 + data = length 195, hash 2722159A + sample 72: + time = 926666 + flags = 1 + data = length 199, hash 10CEE97A + sample 73: + time = 948000 + flags = 1 + data = length 191, hash 2CF9FB3F + sample 74: + time = 969333 + flags = 1 + data = length 197, hash A725DA0 + sample 75: + time = 990666 + flags = 1 + data = length 211, hash D4E5DB9E + sample 76: + time = 1012000 + flags = 1 + data = length 189, hash 1A90F496 + sample 77: + time = 1033333 + flags = 1 + data = length 187, hash 44DB2689 + sample 78: + time = 1054666 + flags = 1 + data = length 197, hash 6D3E5117 + sample 79: + time = 1076000 + flags = 1 + data = length 208, hash 5B57B288 + sample 80: + time = 1097333 + flags = 1 + data = length 198, hash D5FC05 + sample 81: + time = 1118666 + flags = 1 + data = length 192, hash 350BBA45 + sample 82: + time = 1140000 + flags = 1 + data = length 195, hash 5F96F2A8 + sample 83: + time = 1161333 + flags = 1 + data = length 202, hash 61D7CC33 + sample 84: + time = 1182666 + flags = 1 + data = length 202, hash 49D335F2 + sample 85: + time = 1204000 + flags = 1 + data = length 192, hash 2FE9CB1A + sample 86: + time = 1225333 + flags = 1 + data = length 201, hash BF0763B2 + sample 87: + time = 1246666 + flags = 1 + data = length 184, hash AD047421 + sample 88: + time = 1268000 + flags = 1 + data = length 196, hash F9088F14 + sample 89: + time = 1289333 + flags = 1 + data = length 190, hash AC6D38FD + sample 90: + time = 1310666 + flags = 1 + data = length 195, hash 8D1A66D2 + sample 91: + time = 1332000 + flags = 1 + data = length 197, hash B46BFB6B + sample 92: + time = 1353333 + flags = 1 + data = length 195, hash D9761F23 + sample 93: + time = 1374666 + flags = 1 + data = length 204, hash 3391B617 + sample 94: + time = 1396000 + flags = 1 + data = length 42, hash 33A1FB52 + sample 95: + time = 1408000 + flags = 1 + data = length 44, hash 408B146E + sample 96: + time = 1410666 + flags = 1 + data = length 44, hash 171C7E0D + sample 97: + time = 1413333 + flags = 1 + data = length 54, hash 6307E16C + sample 98: + time = 1416000 + flags = 1 + data = length 53, hash 4A319572 + sample 99: + time = 1418666 + flags = 1 + data = length 215, hash BA9C445C + sample 100: + time = 1430666 + flags = 1 + data = length 201, hash 3120D234 + sample 101: + time = 1452000 + flags = 1 + data = length 187, hash DB44993C + sample 102: + time = 1473333 + flags = 1 + data = length 196, hash CF2002D7 + sample 103: + time = 1494666 + flags = 1 + data = length 185, hash E03B5D7 + sample 104: + time = 1516000 + flags = 1 + data = length 187, hash DA399A2C + sample 105: + time = 1537333 + flags = 1 + data = length 191, hash 292AA0DB + sample 106: + time = 1558666 + flags = 1 + data = length 201, hash 221910E0 + sample 107: + time = 1580000 + flags = 1 + data = length 194, hash F4ED7821 + sample 108: + time = 1601333 + flags = 1 + data = length 43, hash FDDA515E + sample 109: + time = 1613333 + flags = 1 + data = length 42, hash F3571C0A + sample 110: + time = 1616000 + flags = 1 + data = length 38, hash 39F910B3 + sample 111: + time = 1618666 + flags = 1 + data = length 41, hash 2D189531 + sample 112: + time = 1621333 + flags = 1 + data = length 43, hash 1F7574DB + sample 113: + time = 1624000 + flags = 1 + data = length 43, hash 644D15E5 + sample 114: + time = 1626666 + flags = 1 + data = length 49, hash E8A0878 + sample 115: + time = 1629333 + flags = 1 + data = length 55, hash DFF2046D + sample 116: + time = 1632000 + flags = 1 + data = length 49, hash 9FB8A23 + sample 117: + time = 1634666 + flags = 1 + data = length 41, hash E3E15E3B + sample 118: + time = 1637333 + flags = 1 + data = length 42, hash E5D17A32 + sample 119: + time = 1640000 + flags = 1 + data = length 42, hash F308B653 + sample 120: + time = 1642666 + flags = 1 + data = length 55, hash BB750D76 + sample 121: + time = 1645333 + flags = 1 + data = length 51, hash 96772ABF + sample 122: + time = 1648000 + flags = 1 + data = length 197, hash E4524346 + sample 123: + time = 1660000 + flags = 1 + data = length 188, hash AC3E1BB5 + sample 124: + time = 1681333 + flags = 1 + data = length 195, hash F56DB8A5 + sample 125: + time = 1702666 + flags = 1 + data = length 198, hash C8970FF7 + sample 126: + time = 1724000 + flags = 1 + data = length 202, hash AF425C68 + sample 127: + time = 1745333 + flags = 1 + data = length 196, hash 4215D839 + sample 128: + time = 1766666 + flags = 1 + data = length 204, hash DB9BE8E3 + sample 129: + time = 1788000 + flags = 1 + data = length 206, hash E5B20AB8 + sample 130: + time = 1809333 + flags = 1 + data = length 209, hash D7F47B95 + sample 131: + time = 1830666 + flags = 1 + data = length 193, hash FB54FB05 + sample 132: + time = 1852000 + flags = 1 + data = length 199, hash D99C3106 + sample 133: + time = 1873333 + flags = 1 + data = length 206, hash 253885B9 + sample 134: + time = 1894666 + flags = 1 + data = length 191, hash FBDD8162 + sample 135: + time = 1916000 + flags = 1 + data = length 183, hash 7290332F + sample 136: + time = 1937333 + flags = 1 + data = length 189, hash 1A9DC3DE + sample 137: + time = 1958666 + flags = 1 + data = length 201, hash 5D936764 + sample 138: + time = 1980000 + flags = 1 + data = length 193, hash 6B03E75E + sample 139: + time = 2001333 + flags = 1 + data = length 199, hash 8A21BA83 + sample 140: + time = 2022666 + flags = 1 + data = length 41, hash E6362210 + sample 141: + time = 2034666 + flags = 1 + data = length 43, hash 36A57B44 + sample 142: + time = 2037333 + flags = 1 + data = length 43, hash E51797D5 + sample 143: + time = 2040000 + flags = 1 + data = length 43, hash 1F336C72 + sample 144: + time = 2042666 + flags = 1 + data = length 42, hash 201AD367 + sample 145: + time = 2045333 + flags = 1 + data = length 50, hash 606CCD6 + sample 146: + time = 2048000 + flags = 1 + data = length 56, hash B15EBD7A + sample 147: + time = 2050666 + flags = 1 + data = length 212, hash 273B8D22 + sample 148: + time = 2062666 + flags = 1 + data = length 194, hash 44F9CE1 + sample 149: + time = 2084000 + flags = 1 + data = length 195, hash EDF9EBA1 + sample 150: + time = 2105333 + flags = 1 + data = length 194, hash CE9F2D26 + sample 151: + time = 2126666 + flags = 1 + data = length 192, hash 204F8A23 + sample 152: + time = 2148000 + flags = 1 + data = length 206, hash DFA57E67 + sample 153: + time = 2169333 + flags = 1 + data = length 196, hash 3CF084AB + sample 154: + time = 2190666 + flags = 1 + data = length 202, hash 2AF75C08 + sample 155: + time = 2212000 + flags = 1 + data = length 203, hash 748EAF7 + sample 156: + time = 2233333 + flags = 1 + data = length 205, hash ED82379D + sample 157: + time = 2254666 + flags = 1 + data = length 193, hash 61F26F22 + sample 158: + time = 2276000 + flags = 1 + data = length 189, hash 85EF1D20 + sample 159: + time = 2297333 + flags = 1 + data = length 187, hash 25E41FBF + sample 160: + time = 2318666 + flags = 1 + data = length 199, hash F365808 + sample 161: + time = 2340000 + flags = 1 + data = length 197, hash 94205329 + sample 162: + time = 2361333 + flags = 1 + data = length 201, hash FA2B2055 + sample 163: + time = 2382666 + flags = 1 + data = length 194, hash AF95381F + sample 164: + time = 2404000 + flags = 1 + data = length 201, hash 923D3534 + sample 165: + time = 2425333 + flags = 1 + data = length 198, hash 35F84C2E + sample 166: + time = 2446666 + flags = 1 + data = length 204, hash 6642CA40 + sample 167: + time = 2468000 + flags = 1 + data = length 183, hash 3E2DC6BE + sample 168: + time = 2489333 + flags = 1 + data = length 197, hash B1E458CE + sample 169: + time = 2510666 + flags = 1 + data = length 193, hash E9218C84 + sample 170: + time = 2532000 + flags = 1 + data = length 192, hash FEF08D4B + sample 171: + time = 2553333 + flags = 1 + data = length 201, hash FC411147 + sample 172: + time = 2574666 + flags = 1 + data = length 218, hash 86893464 + sample 173: + time = 2596000 + flags = 1 + data = length 226, hash 31C5320 + sample 174: + time = 2617333 + flags = 1 + data = length 233, hash 9432BEE5 + sample 175: + time = 2638666 + flags = 1 + data = length 213, hash B3FCC53E + sample 176: + time = 2660000 + flags = 1 + data = length 204, hash D70DD5A2 + sample 177: + time = 2681333 + flags = 1 + data = length 212, hash A4EF1B69 + sample 178: + time = 2702666 + flags = 1 + data = length 203, hash 8B0748B5 + sample 179: + time = 2724000 + flags = 1 + data = length 149, hash E455335B +tracksEnded = true diff --git a/testdata/src/test/assets/extractordumps/ogg/bear_vorbis_with_large_metadata.ogg.1.dump b/testdata/src/test/assets/extractordumps/ogg/bear_vorbis_with_large_metadata.ogg.1.dump new file mode 100644 index 0000000000..1a71ebbb10 --- /dev/null +++ b/testdata/src/test/assets/extractordumps/ogg/bear_vorbis_with_large_metadata.ogg.1.dump @@ -0,0 +1,456 @@ +seekMap: + isSeekable = true + duration = 2741000 + getPosition(0) = [[timeUs=0, position=84969]] + getPosition(1) = [[timeUs=1, position=84969]] + getPosition(1370500) = [[timeUs=1370500, position=84969]] + getPosition(2741000) = [[timeUs=2741000, position=84969]] +numberOfTracks = 1 +track 0: + total output bytes = 17598 + sample count = 109 + format 0: + averageBitrate = 112000 + sampleMimeType = audio/vorbis + channelCount = 2 + sampleRate = 48000 + initializationData: + data = length 30, hash 9A8FF207 + data = length 3832, hash 8A406249 + sample 0: + time = 905333 + flags = 1 + data = length 195, hash 2722159A + sample 1: + time = 926666 + flags = 1 + data = length 199, hash 10CEE97A + sample 2: + time = 948000 + flags = 1 + data = length 191, hash 2CF9FB3F + sample 3: + time = 969333 + flags = 1 + data = length 197, hash A725DA0 + sample 4: + time = 990666 + flags = 1 + data = length 211, hash D4E5DB9E + sample 5: + time = 1012000 + flags = 1 + data = length 189, hash 1A90F496 + sample 6: + time = 1033333 + flags = 1 + data = length 187, hash 44DB2689 + sample 7: + time = 1054666 + flags = 1 + data = length 197, hash 6D3E5117 + sample 8: + time = 1076000 + flags = 1 + data = length 208, hash 5B57B288 + sample 9: + time = 1097333 + flags = 1 + data = length 198, hash D5FC05 + sample 10: + time = 1118666 + flags = 1 + data = length 192, hash 350BBA45 + sample 11: + time = 1140000 + flags = 1 + data = length 195, hash 5F96F2A8 + sample 12: + time = 1161333 + flags = 1 + data = length 202, hash 61D7CC33 + sample 13: + time = 1182666 + flags = 1 + data = length 202, hash 49D335F2 + sample 14: + time = 1204000 + flags = 1 + data = length 192, hash 2FE9CB1A + sample 15: + time = 1225333 + flags = 1 + data = length 201, hash BF0763B2 + sample 16: + time = 1246666 + flags = 1 + data = length 184, hash AD047421 + sample 17: + time = 1268000 + flags = 1 + data = length 196, hash F9088F14 + sample 18: + time = 1289333 + flags = 1 + data = length 190, hash AC6D38FD + sample 19: + time = 1310666 + flags = 1 + data = length 195, hash 8D1A66D2 + sample 20: + time = 1332000 + flags = 1 + data = length 197, hash B46BFB6B + sample 21: + time = 1353333 + flags = 1 + data = length 195, hash D9761F23 + sample 22: + time = 1374666 + flags = 1 + data = length 204, hash 3391B617 + sample 23: + time = 1396000 + flags = 1 + data = length 42, hash 33A1FB52 + sample 24: + time = 1408000 + flags = 1 + data = length 44, hash 408B146E + sample 25: + time = 1410666 + flags = 1 + data = length 44, hash 171C7E0D + sample 26: + time = 1413333 + flags = 1 + data = length 54, hash 6307E16C + sample 27: + time = 1416000 + flags = 1 + data = length 53, hash 4A319572 + sample 28: + time = 1418666 + flags = 1 + data = length 215, hash BA9C445C + sample 29: + time = 1430666 + flags = 1 + data = length 201, hash 3120D234 + sample 30: + time = 1452000 + flags = 1 + data = length 187, hash DB44993C + sample 31: + time = 1473333 + flags = 1 + data = length 196, hash CF2002D7 + sample 32: + time = 1494666 + flags = 1 + data = length 185, hash E03B5D7 + sample 33: + time = 1516000 + flags = 1 + data = length 187, hash DA399A2C + sample 34: + time = 1537333 + flags = 1 + data = length 191, hash 292AA0DB + sample 35: + time = 1558666 + flags = 1 + data = length 201, hash 221910E0 + sample 36: + time = 1580000 + flags = 1 + data = length 194, hash F4ED7821 + sample 37: + time = 1601333 + flags = 1 + data = length 43, hash FDDA515E + sample 38: + time = 1613333 + flags = 1 + data = length 42, hash F3571C0A + sample 39: + time = 1616000 + flags = 1 + data = length 38, hash 39F910B3 + sample 40: + time = 1618666 + flags = 1 + data = length 41, hash 2D189531 + sample 41: + time = 1621333 + flags = 1 + data = length 43, hash 1F7574DB + sample 42: + time = 1624000 + flags = 1 + data = length 43, hash 644D15E5 + sample 43: + time = 1626666 + flags = 1 + data = length 49, hash E8A0878 + sample 44: + time = 1629333 + flags = 1 + data = length 55, hash DFF2046D + sample 45: + time = 1632000 + flags = 1 + data = length 49, hash 9FB8A23 + sample 46: + time = 1634666 + flags = 1 + data = length 41, hash E3E15E3B + sample 47: + time = 1637333 + flags = 1 + data = length 42, hash E5D17A32 + sample 48: + time = 1640000 + flags = 1 + data = length 42, hash F308B653 + sample 49: + time = 1642666 + flags = 1 + data = length 55, hash BB750D76 + sample 50: + time = 1645333 + flags = 1 + data = length 51, hash 96772ABF + sample 51: + time = 1648000 + flags = 1 + data = length 197, hash E4524346 + sample 52: + time = 1660000 + flags = 1 + data = length 188, hash AC3E1BB5 + sample 53: + time = 1681333 + flags = 1 + data = length 195, hash F56DB8A5 + sample 54: + time = 1702666 + flags = 1 + data = length 198, hash C8970FF7 + sample 55: + time = 1724000 + flags = 1 + data = length 202, hash AF425C68 + sample 56: + time = 1745333 + flags = 1 + data = length 196, hash 4215D839 + sample 57: + time = 1766666 + flags = 1 + data = length 204, hash DB9BE8E3 + sample 58: + time = 1788000 + flags = 1 + data = length 206, hash E5B20AB8 + sample 59: + time = 1809333 + flags = 1 + data = length 209, hash D7F47B95 + sample 60: + time = 1830666 + flags = 1 + data = length 193, hash FB54FB05 + sample 61: + time = 1852000 + flags = 1 + data = length 199, hash D99C3106 + sample 62: + time = 1873333 + flags = 1 + data = length 206, hash 253885B9 + sample 63: + time = 1894666 + flags = 1 + data = length 191, hash FBDD8162 + sample 64: + time = 1916000 + flags = 1 + data = length 183, hash 7290332F + sample 65: + time = 1937333 + flags = 1 + data = length 189, hash 1A9DC3DE + sample 66: + time = 1958666 + flags = 1 + data = length 201, hash 5D936764 + sample 67: + time = 1980000 + flags = 1 + data = length 193, hash 6B03E75E + sample 68: + time = 2001333 + flags = 1 + data = length 199, hash 8A21BA83 + sample 69: + time = 2022666 + flags = 1 + data = length 41, hash E6362210 + sample 70: + time = 2034666 + flags = 1 + data = length 43, hash 36A57B44 + sample 71: + time = 2037333 + flags = 1 + data = length 43, hash E51797D5 + sample 72: + time = 2040000 + flags = 1 + data = length 43, hash 1F336C72 + sample 73: + time = 2042666 + flags = 1 + data = length 42, hash 201AD367 + sample 74: + time = 2045333 + flags = 1 + data = length 50, hash 606CCD6 + sample 75: + time = 2048000 + flags = 1 + data = length 56, hash B15EBD7A + sample 76: + time = 2050666 + flags = 1 + data = length 212, hash 273B8D22 + sample 77: + time = 2062666 + flags = 1 + data = length 194, hash 44F9CE1 + sample 78: + time = 2084000 + flags = 1 + data = length 195, hash EDF9EBA1 + sample 79: + time = 2105333 + flags = 1 + data = length 194, hash CE9F2D26 + sample 80: + time = 2126666 + flags = 1 + data = length 192, hash 204F8A23 + sample 81: + time = 2148000 + flags = 1 + data = length 206, hash DFA57E67 + sample 82: + time = 2169333 + flags = 1 + data = length 196, hash 3CF084AB + sample 83: + time = 2190666 + flags = 1 + data = length 202, hash 2AF75C08 + sample 84: + time = 2212000 + flags = 1 + data = length 203, hash 748EAF7 + sample 85: + time = 2233333 + flags = 1 + data = length 205, hash ED82379D + sample 86: + time = 2254666 + flags = 1 + data = length 193, hash 61F26F22 + sample 87: + time = 2276000 + flags = 1 + data = length 189, hash 85EF1D20 + sample 88: + time = 2297333 + flags = 1 + data = length 187, hash 25E41FBF + sample 89: + time = 2318666 + flags = 1 + data = length 199, hash F365808 + sample 90: + time = 2340000 + flags = 1 + data = length 197, hash 94205329 + sample 91: + time = 2361333 + flags = 1 + data = length 201, hash FA2B2055 + sample 92: + time = 2382666 + flags = 1 + data = length 194, hash AF95381F + sample 93: + time = 2404000 + flags = 1 + data = length 201, hash 923D3534 + sample 94: + time = 2425333 + flags = 1 + data = length 198, hash 35F84C2E + sample 95: + time = 2446666 + flags = 1 + data = length 204, hash 6642CA40 + sample 96: + time = 2468000 + flags = 1 + data = length 183, hash 3E2DC6BE + sample 97: + time = 2489333 + flags = 1 + data = length 197, hash B1E458CE + sample 98: + time = 2510666 + flags = 1 + data = length 193, hash E9218C84 + sample 99: + time = 2532000 + flags = 1 + data = length 192, hash FEF08D4B + sample 100: + time = 2553333 + flags = 1 + data = length 201, hash FC411147 + sample 101: + time = 2574666 + flags = 1 + data = length 218, hash 86893464 + sample 102: + time = 2596000 + flags = 1 + data = length 226, hash 31C5320 + sample 103: + time = 2617333 + flags = 1 + data = length 233, hash 9432BEE5 + sample 104: + time = 2638666 + flags = 1 + data = length 213, hash B3FCC53E + sample 105: + time = 2660000 + flags = 1 + data = length 204, hash D70DD5A2 + sample 106: + time = 2681333 + flags = 1 + data = length 212, hash A4EF1B69 + sample 107: + time = 2702666 + flags = 1 + data = length 203, hash 8B0748B5 + sample 108: + time = 2724000 + flags = 1 + data = length 149, hash E455335B +tracksEnded = true diff --git a/testdata/src/test/assets/extractordumps/ogg/bear_vorbis_with_large_metadata.ogg.2.dump b/testdata/src/test/assets/extractordumps/ogg/bear_vorbis_with_large_metadata.ogg.2.dump new file mode 100644 index 0000000000..50b21ade22 --- /dev/null +++ b/testdata/src/test/assets/extractordumps/ogg/bear_vorbis_with_large_metadata.ogg.2.dump @@ -0,0 +1,216 @@ +seekMap: + isSeekable = true + duration = 2741000 + getPosition(0) = [[timeUs=0, position=84969]] + getPosition(1) = [[timeUs=1, position=84969]] + getPosition(1370500) = [[timeUs=1370500, position=84969]] + getPosition(2741000) = [[timeUs=2741000, position=84969]] +numberOfTracks = 1 +track 0: + total output bytes = 8658 + sample count = 49 + format 0: + averageBitrate = 112000 + sampleMimeType = audio/vorbis + channelCount = 2 + sampleRate = 48000 + initializationData: + data = length 30, hash 9A8FF207 + data = length 3832, hash 8A406249 + sample 0: + time = 1821333 + flags = 1 + data = length 193, hash FB54FB05 + sample 1: + time = 1842666 + flags = 1 + data = length 199, hash D99C3106 + sample 2: + time = 1864000 + flags = 1 + data = length 206, hash 253885B9 + sample 3: + time = 1885333 + flags = 1 + data = length 191, hash FBDD8162 + sample 4: + time = 1906666 + flags = 1 + data = length 183, hash 7290332F + sample 5: + time = 1928000 + flags = 1 + data = length 189, hash 1A9DC3DE + sample 6: + time = 1949333 + flags = 1 + data = length 201, hash 5D936764 + sample 7: + time = 1970666 + flags = 1 + data = length 193, hash 6B03E75E + sample 8: + time = 1992000 + flags = 1 + data = length 199, hash 8A21BA83 + sample 9: + time = 2013333 + flags = 1 + data = length 41, hash E6362210 + sample 10: + time = 2025333 + flags = 1 + data = length 43, hash 36A57B44 + sample 11: + time = 2028000 + flags = 1 + data = length 43, hash E51797D5 + sample 12: + time = 2030666 + flags = 1 + data = length 43, hash 1F336C72 + sample 13: + time = 2033333 + flags = 1 + data = length 42, hash 201AD367 + sample 14: + time = 2036000 + flags = 1 + data = length 50, hash 606CCD6 + sample 15: + time = 2038666 + flags = 1 + data = length 56, hash B15EBD7A + sample 16: + time = 2041333 + flags = 1 + data = length 212, hash 273B8D22 + sample 17: + time = 2053333 + flags = 1 + data = length 194, hash 44F9CE1 + sample 18: + time = 2074666 + flags = 1 + data = length 195, hash EDF9EBA1 + sample 19: + time = 2096000 + flags = 1 + data = length 194, hash CE9F2D26 + sample 20: + time = 2117333 + flags = 1 + data = length 192, hash 204F8A23 + sample 21: + time = 2138666 + flags = 1 + data = length 206, hash DFA57E67 + sample 22: + time = 2160000 + flags = 1 + data = length 196, hash 3CF084AB + sample 23: + time = 2181333 + flags = 1 + data = length 202, hash 2AF75C08 + sample 24: + time = 2202666 + flags = 1 + data = length 203, hash 748EAF7 + sample 25: + time = 2224000 + flags = 1 + data = length 205, hash ED82379D + sample 26: + time = 2245333 + flags = 1 + data = length 193, hash 61F26F22 + sample 27: + time = 2266666 + flags = 1 + data = length 189, hash 85EF1D20 + sample 28: + time = 2288000 + flags = 1 + data = length 187, hash 25E41FBF + sample 29: + time = 2309333 + flags = 1 + data = length 199, hash F365808 + sample 30: + time = 2330666 + flags = 1 + data = length 197, hash 94205329 + sample 31: + time = 2352000 + flags = 1 + data = length 201, hash FA2B2055 + sample 32: + time = 2373333 + flags = 1 + data = length 194, hash AF95381F + sample 33: + time = 2394666 + flags = 1 + data = length 201, hash 923D3534 + sample 34: + time = 2416000 + flags = 1 + data = length 198, hash 35F84C2E + sample 35: + time = 2437333 + flags = 1 + data = length 204, hash 6642CA40 + sample 36: + time = 2458666 + flags = 1 + data = length 183, hash 3E2DC6BE + sample 37: + time = 2480000 + flags = 1 + data = length 197, hash B1E458CE + sample 38: + time = 2501333 + flags = 1 + data = length 193, hash E9218C84 + sample 39: + time = 2522666 + flags = 1 + data = length 192, hash FEF08D4B + sample 40: + time = 2544000 + flags = 1 + data = length 201, hash FC411147 + sample 41: + time = 2565333 + flags = 1 + data = length 218, hash 86893464 + sample 42: + time = 2586666 + flags = 1 + data = length 226, hash 31C5320 + sample 43: + time = 2608000 + flags = 1 + data = length 233, hash 9432BEE5 + sample 44: + time = 2629333 + flags = 1 + data = length 213, hash B3FCC53E + sample 45: + time = 2650666 + flags = 1 + data = length 204, hash D70DD5A2 + sample 46: + time = 2672000 + flags = 1 + data = length 212, hash A4EF1B69 + sample 47: + time = 2693333 + flags = 1 + data = length 203, hash 8B0748B5 + sample 48: + time = 2714666 + flags = 1 + data = length 149, hash E455335B +tracksEnded = true diff --git a/testdata/src/test/assets/extractordumps/ogg/bear_vorbis_with_large_metadata.ogg.3.dump b/testdata/src/test/assets/extractordumps/ogg/bear_vorbis_with_large_metadata.ogg.3.dump new file mode 100644 index 0000000000..1d76d892d3 --- /dev/null +++ b/testdata/src/test/assets/extractordumps/ogg/bear_vorbis_with_large_metadata.ogg.3.dump @@ -0,0 +1,20 @@ +seekMap: + isSeekable = true + duration = 2741000 + getPosition(0) = [[timeUs=0, position=84969]] + getPosition(1) = [[timeUs=1, position=84969]] + getPosition(1370500) = [[timeUs=1370500, position=84969]] + getPosition(2741000) = [[timeUs=2741000, position=84969]] +numberOfTracks = 1 +track 0: + total output bytes = 0 + sample count = 0 + format 0: + averageBitrate = 112000 + sampleMimeType = audio/vorbis + channelCount = 2 + sampleRate = 48000 + initializationData: + data = length 30, hash 9A8FF207 + data = length 3832, hash 8A406249 +tracksEnded = true diff --git a/testdata/src/test/assets/extractordumps/ogg/bear_vorbis_with_large_metadata.ogg.unknown_length.dump b/testdata/src/test/assets/extractordumps/ogg/bear_vorbis_with_large_metadata.ogg.unknown_length.dump new file mode 100644 index 0000000000..9830a08357 --- /dev/null +++ b/testdata/src/test/assets/extractordumps/ogg/bear_vorbis_with_large_metadata.ogg.unknown_length.dump @@ -0,0 +1,737 @@ +seekMap: + isSeekable = false + duration = UNSET TIME + getPosition(0) = [[timeUs=0, position=0]] +numberOfTracks = 1 +track 0: + total output bytes = 26873 + sample count = 180 + format 0: + averageBitrate = 112000 + sampleMimeType = audio/vorbis + channelCount = 2 + sampleRate = 48000 + initializationData: + data = length 30, hash 9A8FF207 + data = length 3832, hash 8A406249 + sample 0: + time = 0 + flags = 1 + data = length 49, hash 2FFF94F0 + sample 1: + time = 0 + flags = 1 + data = length 44, hash 3946418A + sample 2: + time = 2666 + flags = 1 + data = length 55, hash 2A0B878E + sample 3: + time = 5333 + flags = 1 + data = length 53, hash CC3B6879 + sample 4: + time = 8000 + flags = 1 + data = length 215, hash 106AE950 + sample 5: + time = 20000 + flags = 1 + data = length 192, hash 2B219F53 + sample 6: + time = 41333 + flags = 1 + data = length 197, hash FBC39422 + sample 7: + time = 62666 + flags = 1 + data = length 209, hash 386E8979 + sample 8: + time = 84000 + flags = 1 + data = length 42, hash E81162C1 + sample 9: + time = 96000 + flags = 1 + data = length 41, hash F15BEE36 + sample 10: + time = 98666 + flags = 1 + data = length 42, hash D67EB19 + sample 11: + time = 101333 + flags = 1 + data = length 42, hash F4DE4792 + sample 12: + time = 104000 + flags = 1 + data = length 53, hash 80F66AC3 + sample 13: + time = 106666 + flags = 1 + data = length 56, hash DCB9DFC4 + sample 14: + time = 109333 + flags = 1 + data = length 55, hash 4E0C4E9D + sample 15: + time = 112000 + flags = 1 + data = length 203, hash 176B6862 + sample 16: + time = 124000 + flags = 1 + data = length 193, hash AB13CB10 + sample 17: + time = 145333 + flags = 1 + data = length 203, hash DE63DE9F + sample 18: + time = 166666 + flags = 1 + data = length 194, hash 4A9508A2 + sample 19: + time = 188000 + flags = 1 + data = length 210, hash 196899B3 + sample 20: + time = 209333 + flags = 1 + data = length 195, hash B68407F1 + sample 21: + time = 230666 + flags = 1 + data = length 193, hash A1FA86E3 + sample 22: + time = 252000 + flags = 1 + data = length 194, hash 5C0B9343 + sample 23: + time = 273333 + flags = 1 + data = length 198, hash 789914B2 + sample 24: + time = 294666 + flags = 1 + data = length 183, hash 1B82D11F + sample 25: + time = 316000 + flags = 1 + data = length 199, hash D5B848F4 + sample 26: + time = 337333 + flags = 1 + data = length 192, hash B34427EA + sample 27: + time = 358666 + flags = 1 + data = length 199, hash C2599BB5 + sample 28: + time = 380000 + flags = 1 + data = length 195, hash BFD83194 + sample 29: + time = 401333 + flags = 1 + data = length 199, hash C9A7F7CA + sample 30: + time = 422666 + flags = 1 + data = length 44, hash 5D76EAD6 + sample 31: + time = 434666 + flags = 1 + data = length 43, hash 8619C423 + sample 32: + time = 437333 + flags = 1 + data = length 43, hash E490BBE + sample 33: + time = 440000 + flags = 1 + data = length 53, hash 8A557CAE + sample 34: + time = 442666 + flags = 1 + data = length 56, hash 81007BBA + sample 35: + time = 445333 + flags = 1 + data = length 56, hash 4E4DD67F + sample 36: + time = 448000 + flags = 1 + data = length 222, hash 414188AB + sample 37: + time = 460000 + flags = 1 + data = length 202, hash 67A07D30 + sample 38: + time = 481333 + flags = 1 + data = length 200, hash E357D853 + sample 39: + time = 502666 + flags = 1 + data = length 203, hash 4653DC90 + sample 40: + time = 524000 + flags = 1 + data = length 192, hash A65E6C09 + sample 41: + time = 545333 + flags = 1 + data = length 202, hash FBEAC508 + sample 42: + time = 566666 + flags = 1 + data = length 202, hash E9B7B59F + sample 43: + time = 588000 + flags = 1 + data = length 204, hash E24AA78E + sample 44: + time = 609333 + flags = 1 + data = length 41, hash 3FBC5216 + sample 45: + time = 621333 + flags = 1 + data = length 47, hash 153FBC55 + sample 46: + time = 624000 + flags = 1 + data = length 42, hash 2B493D6C + sample 47: + time = 626666 + flags = 1 + data = length 42, hash 8303BEE3 + sample 48: + time = 629333 + flags = 1 + data = length 62, hash 71AEE50B + sample 49: + time = 632000 + flags = 1 + data = length 54, hash 52F61908 + sample 50: + time = 634666 + flags = 1 + data = length 45, hash 7BD3E3A1 + sample 51: + time = 637333 + flags = 1 + data = length 41, hash E0F65472 + sample 52: + time = 640000 + flags = 1 + data = length 45, hash 41838675 + sample 53: + time = 642666 + flags = 1 + data = length 44, hash FCBC2147 + sample 54: + time = 645333 + flags = 1 + data = length 45, hash 1A5987E3 + sample 55: + time = 648000 + flags = 1 + data = length 43, hash 99074864 + sample 56: + time = 650666 + flags = 1 + data = length 57, hash D4A9B60A + sample 57: + time = 653333 + flags = 1 + data = length 52, hash 302129DA + sample 58: + time = 656000 + flags = 1 + data = length 57, hash D8DD99C0 + sample 59: + time = 658666 + flags = 1 + data = length 206, hash F4B9EF26 + sample 60: + time = 670666 + flags = 1 + data = length 197, hash 7B8ACC8A + sample 61: + time = 692000 + flags = 1 + data = length 186, hash 161027CB + sample 62: + time = 713333 + flags = 1 + data = length 186, hash 1D6871B6 + sample 63: + time = 734666 + flags = 1 + data = length 201, hash 536E9FDB + sample 64: + time = 756000 + flags = 1 + data = length 192, hash D38EFAC5 + sample 65: + time = 777333 + flags = 1 + data = length 194, hash 4B394EF3 + sample 66: + time = 798666 + flags = 1 + data = length 206, hash 1B31BA99 + sample 67: + time = 820000 + flags = 1 + data = length 212, hash AD061F43 + sample 68: + time = 841333 + flags = 1 + data = length 180, hash 6D1F7481 + sample 69: + time = 862666 + flags = 1 + data = length 195, hash D80B21F + sample 70: + time = 884000 + flags = 1 + data = length 186, hash D367882 + sample 71: + time = 905333 + flags = 1 + data = length 195, hash 2722159A + sample 72: + time = 926666 + flags = 1 + data = length 199, hash 10CEE97A + sample 73: + time = 948000 + flags = 1 + data = length 191, hash 2CF9FB3F + sample 74: + time = 969333 + flags = 1 + data = length 197, hash A725DA0 + sample 75: + time = 990666 + flags = 1 + data = length 211, hash D4E5DB9E + sample 76: + time = 1012000 + flags = 1 + data = length 189, hash 1A90F496 + sample 77: + time = 1033333 + flags = 1 + data = length 187, hash 44DB2689 + sample 78: + time = 1054666 + flags = 1 + data = length 197, hash 6D3E5117 + sample 79: + time = 1076000 + flags = 1 + data = length 208, hash 5B57B288 + sample 80: + time = 1097333 + flags = 1 + data = length 198, hash D5FC05 + sample 81: + time = 1118666 + flags = 1 + data = length 192, hash 350BBA45 + sample 82: + time = 1140000 + flags = 1 + data = length 195, hash 5F96F2A8 + sample 83: + time = 1161333 + flags = 1 + data = length 202, hash 61D7CC33 + sample 84: + time = 1182666 + flags = 1 + data = length 202, hash 49D335F2 + sample 85: + time = 1204000 + flags = 1 + data = length 192, hash 2FE9CB1A + sample 86: + time = 1225333 + flags = 1 + data = length 201, hash BF0763B2 + sample 87: + time = 1246666 + flags = 1 + data = length 184, hash AD047421 + sample 88: + time = 1268000 + flags = 1 + data = length 196, hash F9088F14 + sample 89: + time = 1289333 + flags = 1 + data = length 190, hash AC6D38FD + sample 90: + time = 1310666 + flags = 1 + data = length 195, hash 8D1A66D2 + sample 91: + time = 1332000 + flags = 1 + data = length 197, hash B46BFB6B + sample 92: + time = 1353333 + flags = 1 + data = length 195, hash D9761F23 + sample 93: + time = 1374666 + flags = 1 + data = length 204, hash 3391B617 + sample 94: + time = 1396000 + flags = 1 + data = length 42, hash 33A1FB52 + sample 95: + time = 1408000 + flags = 1 + data = length 44, hash 408B146E + sample 96: + time = 1410666 + flags = 1 + data = length 44, hash 171C7E0D + sample 97: + time = 1413333 + flags = 1 + data = length 54, hash 6307E16C + sample 98: + time = 1416000 + flags = 1 + data = length 53, hash 4A319572 + sample 99: + time = 1418666 + flags = 1 + data = length 215, hash BA9C445C + sample 100: + time = 1430666 + flags = 1 + data = length 201, hash 3120D234 + sample 101: + time = 1452000 + flags = 1 + data = length 187, hash DB44993C + sample 102: + time = 1473333 + flags = 1 + data = length 196, hash CF2002D7 + sample 103: + time = 1494666 + flags = 1 + data = length 185, hash E03B5D7 + sample 104: + time = 1516000 + flags = 1 + data = length 187, hash DA399A2C + sample 105: + time = 1537333 + flags = 1 + data = length 191, hash 292AA0DB + sample 106: + time = 1558666 + flags = 1 + data = length 201, hash 221910E0 + sample 107: + time = 1580000 + flags = 1 + data = length 194, hash F4ED7821 + sample 108: + time = 1601333 + flags = 1 + data = length 43, hash FDDA515E + sample 109: + time = 1613333 + flags = 1 + data = length 42, hash F3571C0A + sample 110: + time = 1616000 + flags = 1 + data = length 38, hash 39F910B3 + sample 111: + time = 1618666 + flags = 1 + data = length 41, hash 2D189531 + sample 112: + time = 1621333 + flags = 1 + data = length 43, hash 1F7574DB + sample 113: + time = 1624000 + flags = 1 + data = length 43, hash 644D15E5 + sample 114: + time = 1626666 + flags = 1 + data = length 49, hash E8A0878 + sample 115: + time = 1629333 + flags = 1 + data = length 55, hash DFF2046D + sample 116: + time = 1632000 + flags = 1 + data = length 49, hash 9FB8A23 + sample 117: + time = 1634666 + flags = 1 + data = length 41, hash E3E15E3B + sample 118: + time = 1637333 + flags = 1 + data = length 42, hash E5D17A32 + sample 119: + time = 1640000 + flags = 1 + data = length 42, hash F308B653 + sample 120: + time = 1642666 + flags = 1 + data = length 55, hash BB750D76 + sample 121: + time = 1645333 + flags = 1 + data = length 51, hash 96772ABF + sample 122: + time = 1648000 + flags = 1 + data = length 197, hash E4524346 + sample 123: + time = 1660000 + flags = 1 + data = length 188, hash AC3E1BB5 + sample 124: + time = 1681333 + flags = 1 + data = length 195, hash F56DB8A5 + sample 125: + time = 1702666 + flags = 1 + data = length 198, hash C8970FF7 + sample 126: + time = 1724000 + flags = 1 + data = length 202, hash AF425C68 + sample 127: + time = 1745333 + flags = 1 + data = length 196, hash 4215D839 + sample 128: + time = 1766666 + flags = 1 + data = length 204, hash DB9BE8E3 + sample 129: + time = 1788000 + flags = 1 + data = length 206, hash E5B20AB8 + sample 130: + time = 1809333 + flags = 1 + data = length 209, hash D7F47B95 + sample 131: + time = 1830666 + flags = 1 + data = length 193, hash FB54FB05 + sample 132: + time = 1852000 + flags = 1 + data = length 199, hash D99C3106 + sample 133: + time = 1873333 + flags = 1 + data = length 206, hash 253885B9 + sample 134: + time = 1894666 + flags = 1 + data = length 191, hash FBDD8162 + sample 135: + time = 1916000 + flags = 1 + data = length 183, hash 7290332F + sample 136: + time = 1937333 + flags = 1 + data = length 189, hash 1A9DC3DE + sample 137: + time = 1958666 + flags = 1 + data = length 201, hash 5D936764 + sample 138: + time = 1980000 + flags = 1 + data = length 193, hash 6B03E75E + sample 139: + time = 2001333 + flags = 1 + data = length 199, hash 8A21BA83 + sample 140: + time = 2022666 + flags = 1 + data = length 41, hash E6362210 + sample 141: + time = 2034666 + flags = 1 + data = length 43, hash 36A57B44 + sample 142: + time = 2037333 + flags = 1 + data = length 43, hash E51797D5 + sample 143: + time = 2040000 + flags = 1 + data = length 43, hash 1F336C72 + sample 144: + time = 2042666 + flags = 1 + data = length 42, hash 201AD367 + sample 145: + time = 2045333 + flags = 1 + data = length 50, hash 606CCD6 + sample 146: + time = 2048000 + flags = 1 + data = length 56, hash B15EBD7A + sample 147: + time = 2050666 + flags = 1 + data = length 212, hash 273B8D22 + sample 148: + time = 2062666 + flags = 1 + data = length 194, hash 44F9CE1 + sample 149: + time = 2084000 + flags = 1 + data = length 195, hash EDF9EBA1 + sample 150: + time = 2105333 + flags = 1 + data = length 194, hash CE9F2D26 + sample 151: + time = 2126666 + flags = 1 + data = length 192, hash 204F8A23 + sample 152: + time = 2148000 + flags = 1 + data = length 206, hash DFA57E67 + sample 153: + time = 2169333 + flags = 1 + data = length 196, hash 3CF084AB + sample 154: + time = 2190666 + flags = 1 + data = length 202, hash 2AF75C08 + sample 155: + time = 2212000 + flags = 1 + data = length 203, hash 748EAF7 + sample 156: + time = 2233333 + flags = 1 + data = length 205, hash ED82379D + sample 157: + time = 2254666 + flags = 1 + data = length 193, hash 61F26F22 + sample 158: + time = 2276000 + flags = 1 + data = length 189, hash 85EF1D20 + sample 159: + time = 2297333 + flags = 1 + data = length 187, hash 25E41FBF + sample 160: + time = 2318666 + flags = 1 + data = length 199, hash F365808 + sample 161: + time = 2340000 + flags = 1 + data = length 197, hash 94205329 + sample 162: + time = 2361333 + flags = 1 + data = length 201, hash FA2B2055 + sample 163: + time = 2382666 + flags = 1 + data = length 194, hash AF95381F + sample 164: + time = 2404000 + flags = 1 + data = length 201, hash 923D3534 + sample 165: + time = 2425333 + flags = 1 + data = length 198, hash 35F84C2E + sample 166: + time = 2446666 + flags = 1 + data = length 204, hash 6642CA40 + sample 167: + time = 2468000 + flags = 1 + data = length 183, hash 3E2DC6BE + sample 168: + time = 2489333 + flags = 1 + data = length 197, hash B1E458CE + sample 169: + time = 2510666 + flags = 1 + data = length 193, hash E9218C84 + sample 170: + time = 2532000 + flags = 1 + data = length 192, hash FEF08D4B + sample 171: + time = 2553333 + flags = 1 + data = length 201, hash FC411147 + sample 172: + time = 2574666 + flags = 1 + data = length 218, hash 86893464 + sample 173: + time = 2596000 + flags = 1 + data = length 226, hash 31C5320 + sample 174: + time = 2617333 + flags = 1 + data = length 233, hash 9432BEE5 + sample 175: + time = 2638666 + flags = 1 + data = length 213, hash B3FCC53E + sample 176: + time = 2660000 + flags = 1 + data = length 204, hash D70DD5A2 + sample 177: + time = 2681333 + flags = 1 + data = length 212, hash A4EF1B69 + sample 178: + time = 2702666 + flags = 1 + data = length 203, hash 8B0748B5 + sample 179: + time = 2724000 + flags = 1 + data = length 149, hash E455335B +tracksEnded = true diff --git a/testdata/src/test/assets/media/ogg/bear_vorbis_with_large_metadata.ogg b/testdata/src/test/assets/media/ogg/bear_vorbis_with_large_metadata.ogg new file mode 100644 index 0000000000000000000000000000000000000000..1dab9e5258f29ca10e11aaf115fd06b5616e4ab0 GIT binary patch literal 111383 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 OCUj~chimUR=>GxJczsv^ literal 0 HcmV?d00001 From 9ce2ac8a8cb2b45426b3fe269e9845b8b9d0daa6 Mon Sep 17 00:00:00 2001 From: ibaker Date: Wed, 30 Sep 2020 16:29:38 +0100 Subject: [PATCH 074/110] Create a robolectricutils module This holds shared test infrastructure that needs to depend on Robolectric. PiperOrigin-RevId: 334604041 --- core_settings.gradle | 2 ++ library/core/build.gradle | 1 + .../exoplayer2/e2etest/Mp4PlaybackTest.java | 4 +-- .../exoplayer2/e2etest/TsPlaybackTest.java | 4 +-- robolectricutils/README.md | 10 ++++++ robolectricutils/build.gradle | 35 +++++++++++++++++++ robolectricutils/src/main/AndroidManifest.xml | 17 +++++++++ .../robolectric}/PlaybackOutput.java | 2 +- .../robolectric}/ShadowMediaCodecConfig.java | 2 +- .../exoplayer2/robolectric}/TeeCodec.java | 2 +- .../exoplayer2/robolectric/package-info.java | 19 ++++++++++ 11 files changed, 91 insertions(+), 7 deletions(-) create mode 100644 robolectricutils/README.md create mode 100644 robolectricutils/build.gradle create mode 100644 robolectricutils/src/main/AndroidManifest.xml rename {library/core/src/test/java/com/google/android/exoplayer2/e2etest/util => robolectricutils/src/main/java/com/google/android/exoplayer2/robolectric}/PlaybackOutput.java (98%) rename {library/core/src/test/java/com/google/android/exoplayer2/e2etest/util => robolectricutils/src/main/java/com/google/android/exoplayer2/robolectric}/ShadowMediaCodecConfig.java (98%) rename {library/core/src/test/java/com/google/android/exoplayer2/e2etest/util => robolectricutils/src/main/java/com/google/android/exoplayer2/robolectric}/TeeCodec.java (98%) create mode 100644 robolectricutils/src/main/java/com/google/android/exoplayer2/robolectric/package-info.java diff --git a/core_settings.gradle b/core_settings.gradle index b508243371..bd217a37e5 100644 --- a/core_settings.gradle +++ b/core_settings.gradle @@ -29,6 +29,7 @@ include modulePrefix + 'library-extractor' include modulePrefix + 'library-hls' include modulePrefix + 'library-smoothstreaming' include modulePrefix + 'library-ui' +include modulePrefix + 'robolectricutils' include modulePrefix + 'testutils' include modulePrefix + 'testdata' include modulePrefix + 'extension-av1' @@ -56,6 +57,7 @@ project(modulePrefix + 'library-extractor').projectDir = new File(rootDir, 'libr project(modulePrefix + 'library-hls').projectDir = new File(rootDir, 'library/hls') project(modulePrefix + 'library-smoothstreaming').projectDir = new File(rootDir, 'library/smoothstreaming') project(modulePrefix + 'library-ui').projectDir = new File(rootDir, 'library/ui') +project(modulePrefix + 'robolectricutils').projectDir = new File(rootDir, 'robolectricutils') project(modulePrefix + 'testutils').projectDir = new File(rootDir, 'testutils') project(modulePrefix + 'testdata').projectDir = new File(rootDir, 'testdata') project(modulePrefix + 'extension-av1').projectDir = new File(rootDir, 'extensions/av1') diff --git a/library/core/build.gradle b/library/core/build.gradle index ddeb734947..45c8e785c6 100644 --- a/library/core/build.gradle +++ b/library/core/build.gradle @@ -71,6 +71,7 @@ dependencies { testImplementation 'com.squareup.okhttp3:mockwebserver:' + mockWebServerVersion testImplementation 'org.robolectric:robolectric:' + robolectricVersion testImplementation project(modulePrefix + 'testutils') + testImplementation project(modulePrefix + 'robolectricutils') } ext { diff --git a/library/core/src/test/java/com/google/android/exoplayer2/e2etest/Mp4PlaybackTest.java b/library/core/src/test/java/com/google/android/exoplayer2/e2etest/Mp4PlaybackTest.java index f37610d982..5fd7453beb 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/e2etest/Mp4PlaybackTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/e2etest/Mp4PlaybackTest.java @@ -23,8 +23,8 @@ import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.android.exoplayer2.MediaItem; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.SimpleExoPlayer; -import com.google.android.exoplayer2.e2etest.util.PlaybackOutput; -import com.google.android.exoplayer2.e2etest.util.ShadowMediaCodecConfig; +import com.google.android.exoplayer2.robolectric.PlaybackOutput; +import com.google.android.exoplayer2.robolectric.ShadowMediaCodecConfig; import com.google.android.exoplayer2.testutil.AutoAdvancingFakeClock; import com.google.android.exoplayer2.testutil.DumpFileAsserts; import com.google.android.exoplayer2.testutil.TestExoPlayer; diff --git a/library/core/src/test/java/com/google/android/exoplayer2/e2etest/TsPlaybackTest.java b/library/core/src/test/java/com/google/android/exoplayer2/e2etest/TsPlaybackTest.java index d57f06ff52..52184f5751 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/e2etest/TsPlaybackTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/e2etest/TsPlaybackTest.java @@ -22,8 +22,8 @@ import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.android.exoplayer2.MediaItem; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.SimpleExoPlayer; -import com.google.android.exoplayer2.e2etest.util.PlaybackOutput; -import com.google.android.exoplayer2.e2etest.util.ShadowMediaCodecConfig; +import com.google.android.exoplayer2.robolectric.PlaybackOutput; +import com.google.android.exoplayer2.robolectric.ShadowMediaCodecConfig; import com.google.android.exoplayer2.testutil.AutoAdvancingFakeClock; import com.google.android.exoplayer2.testutil.DumpFileAsserts; import com.google.android.exoplayer2.testutil.TestExoPlayer; diff --git a/robolectricutils/README.md b/robolectricutils/README.md new file mode 100644 index 0000000000..430a907c2d --- /dev/null +++ b/robolectricutils/README.md @@ -0,0 +1,10 @@ +# ExoPlayer Robolectric utils + +Provides test infrastructure for ExoPlayer Robolectric-based tests. + +## Links + +* [Javadoc][]: Classes matching `com.google.android.exoplayer2.robolectric` + belong to this module. + +[Javadoc]: https://exoplayer.dev/doc/reference/index.html diff --git a/robolectricutils/build.gradle b/robolectricutils/build.gradle new file mode 100644 index 0000000000..f5a86822b7 --- /dev/null +++ b/robolectricutils/build.gradle @@ -0,0 +1,35 @@ +// Copyright (C) 2017 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +apply from: "$gradle.ext.exoplayerSettingsDir/common_library_config.gradle" + +dependencies { + compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion + compileOnly 'org.checkerframework:checker-compat-qual:' + checkerframeworkCompatVersion + compileOnly 'org.jetbrains.kotlin:kotlin-annotations-jvm:' + kotlinAnnotationsVersion + implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion + implementation 'org.robolectric:robolectric:' + robolectricVersion + implementation project(modulePrefix + 'library-core') + implementation project(modulePrefix + 'testutils') +} + +ext { + javadocTitle = 'Robolectric utils' +} +apply from: '../javadoc_library.gradle' + +ext { + releaseArtifact = 'exoplayer-robolectricutils' + releaseDescription = 'Robolectric utils for ExoPlayer.' +} +apply from: '../publish.gradle' diff --git a/robolectricutils/src/main/AndroidManifest.xml b/robolectricutils/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..0548a1b32b --- /dev/null +++ b/robolectricutils/src/main/AndroidManifest.xml @@ -0,0 +1,17 @@ + + + + diff --git a/library/core/src/test/java/com/google/android/exoplayer2/e2etest/util/PlaybackOutput.java b/robolectricutils/src/main/java/com/google/android/exoplayer2/robolectric/PlaybackOutput.java similarity index 98% rename from library/core/src/test/java/com/google/android/exoplayer2/e2etest/util/PlaybackOutput.java rename to robolectricutils/src/main/java/com/google/android/exoplayer2/robolectric/PlaybackOutput.java index f9c32d34b5..264b4bcc2f 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/e2etest/util/PlaybackOutput.java +++ b/robolectricutils/src/main/java/com/google/android/exoplayer2/robolectric/PlaybackOutput.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.google.android.exoplayer2.e2etest.util; +package com.google.android.exoplayer2.robolectric; import com.google.android.exoplayer2.SimpleExoPlayer; import com.google.android.exoplayer2.metadata.Metadata; diff --git a/library/core/src/test/java/com/google/android/exoplayer2/e2etest/util/ShadowMediaCodecConfig.java b/robolectricutils/src/main/java/com/google/android/exoplayer2/robolectric/ShadowMediaCodecConfig.java similarity index 98% rename from library/core/src/test/java/com/google/android/exoplayer2/e2etest/util/ShadowMediaCodecConfig.java rename to robolectricutils/src/main/java/com/google/android/exoplayer2/robolectric/ShadowMediaCodecConfig.java index 6d7f23107e..697c4e8316 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/e2etest/util/ShadowMediaCodecConfig.java +++ b/robolectricutils/src/main/java/com/google/android/exoplayer2/robolectric/ShadowMediaCodecConfig.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.google.android.exoplayer2.e2etest.util; +package com.google.android.exoplayer2.robolectric; import android.media.MediaCodecInfo; import android.media.MediaFormat; diff --git a/library/core/src/test/java/com/google/android/exoplayer2/e2etest/util/TeeCodec.java b/robolectricutils/src/main/java/com/google/android/exoplayer2/robolectric/TeeCodec.java similarity index 98% rename from library/core/src/test/java/com/google/android/exoplayer2/e2etest/util/TeeCodec.java rename to robolectricutils/src/main/java/com/google/android/exoplayer2/robolectric/TeeCodec.java index a14787e959..172350414e 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/e2etest/util/TeeCodec.java +++ b/robolectricutils/src/main/java/com/google/android/exoplayer2/robolectric/TeeCodec.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.google.android.exoplayer2.e2etest.util; +package com.google.android.exoplayer2.robolectric; import com.google.android.exoplayer2.testutil.Dumper; import com.google.android.exoplayer2.util.MimeTypes; diff --git a/robolectricutils/src/main/java/com/google/android/exoplayer2/robolectric/package-info.java b/robolectricutils/src/main/java/com/google/android/exoplayer2/robolectric/package-info.java new file mode 100644 index 0000000000..0dd7ab81ae --- /dev/null +++ b/robolectricutils/src/main/java/com/google/android/exoplayer2/robolectric/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@NonNullApi +package com.google.android.exoplayer2.robolectric; + +import com.google.android.exoplayer2.util.NonNullApi; From b8c8ce0ee0c9c6cd32b947ec5da3d4fe6c983864 Mon Sep 17 00:00:00 2001 From: ibaker Date: Thu, 1 Oct 2020 10:22:06 +0100 Subject: [PATCH 075/110] Use Mp4WebvttDecoder for WebVTT content in DASH MP4 containers This was broken by https://github.com/google/ExoPlayer/commit/74a9d8f680995f2096c59fde6cd1ef6e85bb4d55 because DashManifestParser switched to setting Format.sampleMimeType to text/vtt while SubtitleDecoderFactory was still expecting application/x-mp4-vtt. This change teaches SubtitleDecoderFactory to check both Format.containerMimeType and Format.sampleMimeType. I'll investigate a follow-up change to remove MimeTypes.APPLICATION_MP4VTT completely (it's currently still used in AtomParsers). Issue: #7985 PiperOrigin-RevId: 334771672 --- RELEASENOTES.md | 2 + .../text/SubtitleDecoderFactory.java | 16 ++-- library/dash/build.gradle | 1 + .../exoplayer2/e2etest/DashPlaybackTest.java | 73 ++++++++++++++++++ .../robolectric/PlaybackOutput.java | 63 ++++++++++++++- .../media/dash/webvtt-in-mp4/sample.mpd | 23 ++++++ .../media/dash/webvtt-in-mp4/sample.text.mp4 | Bin 0 -> 1006 bytes .../media/dash/webvtt-in-mp4/sample.video.mp4 | Bin 0 -> 91242 bytes .../playbackdumps/dash/webvtt-in-mp4.dump | 58 ++++++++++++++ 9 files changed, 229 insertions(+), 7 deletions(-) create mode 100644 library/dash/src/test/java/com/google/android/exoplayer2/e2etest/DashPlaybackTest.java create mode 100644 testdata/src/test/assets/media/dash/webvtt-in-mp4/sample.mpd create mode 100644 testdata/src/test/assets/media/dash/webvtt-in-mp4/sample.text.mp4 create mode 100644 testdata/src/test/assets/media/dash/webvtt-in-mp4/sample.video.mp4 create mode 100644 testdata/src/test/assets/playbackdumps/dash/webvtt-in-mp4.dump diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 3404a3e7d9..53e4e257e7 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -11,6 +11,8 @@ ([#7866](https://github.com/google/ExoPlayer/issues/7866)). * Text: * Add support for `\h` SSA/ASS style override code (non-breaking space). + * Fix WebVTT subtitles in MP4 containers in DASH streams + ([#7985](https://github.com/google/ExoPlayer/issues/7985)). * UI: * Do not require subtitleButton in custom layouts of StyledPlayerView ([#7962](https://github.com/google/ExoPlayer/issues/7962)). diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/SubtitleDecoderFactory.java b/library/core/src/main/java/com/google/android/exoplayer2/text/SubtitleDecoderFactory.java index bd652c6586..e59a7489bb 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/SubtitleDecoderFactory.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/SubtitleDecoderFactory.java @@ -91,11 +91,15 @@ public interface SubtitleDecoderFactory { @Override public SubtitleDecoder createDecoder(Format format) { - @Nullable String mimeType = format.sampleMimeType; - if (mimeType != null) { - switch (mimeType) { + @Nullable String sampleMimeType = format.sampleMimeType; + if (sampleMimeType != null) { + switch (sampleMimeType) { case MimeTypes.TEXT_VTT: - return new WebvttDecoder(); + if (MimeTypes.APPLICATION_MP4.equals(format.containerMimeType)) { + return new Mp4WebvttDecoder(); + } else { + return new WebvttDecoder(); + } case MimeTypes.TEXT_SSA: return new SsaDecoder(format.initializationData); case MimeTypes.APPLICATION_MP4VTT: @@ -109,7 +113,7 @@ public interface SubtitleDecoderFactory { case MimeTypes.APPLICATION_CEA608: case MimeTypes.APPLICATION_MP4CEA608: return new Cea608Decoder( - mimeType, + sampleMimeType, format.accessibilityChannel, Cea608Decoder.MIN_DATA_CHANNEL_TIMEOUT_MS); case MimeTypes.APPLICATION_CEA708: @@ -123,7 +127,7 @@ public interface SubtitleDecoderFactory { } } throw new IllegalArgumentException( - "Attempted to create decoder for unsupported MIME type: " + mimeType); + "Attempted to create decoder for unsupported MIME type: " + sampleMimeType); } }; } diff --git a/library/dash/build.gradle b/library/dash/build.gradle index e6cb20d933..e34ab3f9db 100644 --- a/library/dash/build.gradle +++ b/library/dash/build.gradle @@ -36,6 +36,7 @@ dependencies { compileOnly 'org.checkerframework:checker-compat-qual:' + checkerframeworkCompatVersion compileOnly 'org.jetbrains.kotlin:kotlin-annotations-jvm:' + kotlinAnnotationsVersion implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion + testImplementation project(modulePrefix + 'robolectricutils') testImplementation project(modulePrefix + 'testutils') testImplementation 'org.robolectric:robolectric:' + robolectricVersion } diff --git a/library/dash/src/test/java/com/google/android/exoplayer2/e2etest/DashPlaybackTest.java b/library/dash/src/test/java/com/google/android/exoplayer2/e2etest/DashPlaybackTest.java new file mode 100644 index 0000000000..e0ea43b114 --- /dev/null +++ b/library/dash/src/test/java/com/google/android/exoplayer2/e2etest/DashPlaybackTest.java @@ -0,0 +1,73 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.android.exoplayer2.e2etest; + +import static com.google.android.exoplayer2.util.Assertions.checkNotNull; + +import android.graphics.SurfaceTexture; +import android.view.Surface; +import androidx.test.core.app.ApplicationProvider; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.google.android.exoplayer2.MediaItem; +import com.google.android.exoplayer2.Player; +import com.google.android.exoplayer2.SimpleExoPlayer; +import com.google.android.exoplayer2.robolectric.PlaybackOutput; +import com.google.android.exoplayer2.robolectric.ShadowMediaCodecConfig; +import com.google.android.exoplayer2.testutil.AutoAdvancingFakeClock; +import com.google.android.exoplayer2.testutil.DumpFileAsserts; +import com.google.android.exoplayer2.testutil.TestExoPlayer; +import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.annotation.Config; + +/** End-to-end tests using DASH samples. */ +// TODO(b/143232359): Remove once https://issuetracker.google.com/143232359 is resolved. +@Config(sdk = 29) +@RunWith(AndroidJUnit4.class) +public final class DashPlaybackTest { + + @Rule + public ShadowMediaCodecConfig mediaCodecConfig = + ShadowMediaCodecConfig.forAllSupportedMimeTypes(); + + // https://github.com/google/ExoPlayer/issues/7985 + @Test + public void webvttInMp4() throws Exception { + SimpleExoPlayer player = + new SimpleExoPlayer.Builder(ApplicationProvider.getApplicationContext()) + .setClock(new AutoAdvancingFakeClock()) + .build(); + player.setVideoSurface(new Surface(new SurfaceTexture(/* texName= */ 1))); + PlaybackOutput playbackOutput = PlaybackOutput.register(player, mediaCodecConfig); + + // Ensure the subtitle track is selected. + DefaultTrackSelector trackSelector = + checkNotNull((DefaultTrackSelector) player.getTrackSelector()); + trackSelector.setParameters(trackSelector.buildUponParameters().setPreferredTextLanguage("en")); + player.setMediaItem(MediaItem.fromUri("asset:///media/dash/webvtt-in-mp4/sample.mpd")); + player.prepare(); + player.play(); + TestExoPlayer.runUntilPlaybackState(player, Player.STATE_ENDED); + player.release(); + + DumpFileAsserts.assertOutput( + ApplicationProvider.getApplicationContext(), + playbackOutput, + "playbackdumps/dash/webvtt-in-mp4.dump"); + } +} diff --git a/robolectricutils/src/main/java/com/google/android/exoplayer2/robolectric/PlaybackOutput.java b/robolectricutils/src/main/java/com/google/android/exoplayer2/robolectric/PlaybackOutput.java index 264b4bcc2f..64ff61cb22 100644 --- a/robolectricutils/src/main/java/com/google/android/exoplayer2/robolectric/PlaybackOutput.java +++ b/robolectricutils/src/main/java/com/google/android/exoplayer2/robolectric/PlaybackOutput.java @@ -15,12 +15,17 @@ */ package com.google.android.exoplayer2.robolectric; +import android.graphics.Bitmap; +import androidx.annotation.Nullable; import com.google.android.exoplayer2.SimpleExoPlayer; import com.google.android.exoplayer2.metadata.Metadata; import com.google.android.exoplayer2.testutil.Dumper; +import com.google.android.exoplayer2.text.Cue; import com.google.android.exoplayer2.util.Assertions; +import com.google.android.exoplayer2.util.Util; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; +import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -35,17 +40,19 @@ public final class PlaybackOutput implements Dumper.Dumpable { private final ShadowMediaCodecConfig codecConfig; - // TODO: Add support for subtitles too private final List metadatas; + private final List> subtitles; private PlaybackOutput(SimpleExoPlayer player, ShadowMediaCodecConfig codecConfig) { this.codecConfig = codecConfig; metadatas = Collections.synchronizedList(new ArrayList<>()); + subtitles = Collections.synchronizedList(new ArrayList<>()); // TODO: Consider passing playback position into MetadataOutput and TextOutput. Calling // player.getCurrentPosition() inside onMetadata/Cues will likely be non-deterministic // because renderer-thread != playback-thread. player.addMetadataOutput(metadatas::add); + player.addTextOutput(subtitles::add); } /** @@ -74,6 +81,7 @@ public final class PlaybackOutput implements Dumper.Dumpable { } dumpMetadata(dumper); + dumpSubtitles(dumper); } private void dumpMetadata(Dumper dumper) { @@ -91,4 +99,57 @@ public final class PlaybackOutput implements Dumper.Dumpable { } dumper.endBlock(); } + + private void dumpSubtitles(Dumper dumper) { + if (subtitles.isEmpty()) { + return; + } + dumper.startBlock("TextOutput"); + for (int i = 0; i < subtitles.size(); i++) { + dumper.startBlock("Subtitle[" + i + "]"); + List subtitle = subtitles.get(i); + if (subtitle.isEmpty()) { + dumper.add("Cues", ImmutableList.of()); + } + for (int j = 0; j < subtitle.size(); j++) { + dumper.startBlock("Cue[" + j + "]"); + Cue cue = subtitle.get(j); + dumpIfNotEqual(dumper, "text", cue.text, null); + dumpIfNotEqual(dumper, "textAlignment", cue.textAlignment, null); + dumpBitmap(dumper, cue.bitmap); + dumpIfNotEqual(dumper, "line", cue.line, Cue.DIMEN_UNSET); + dumpIfNotEqual(dumper, "lineType", cue.lineType, Cue.TYPE_UNSET); + dumpIfNotEqual(dumper, "lineAnchor", cue.lineAnchor, Cue.TYPE_UNSET); + dumpIfNotEqual(dumper, "position", cue.position, Cue.DIMEN_UNSET); + dumpIfNotEqual(dumper, "positionAnchor", cue.positionAnchor, Cue.TYPE_UNSET); + dumpIfNotEqual(dumper, "size", cue.size, Cue.DIMEN_UNSET); + dumpIfNotEqual(dumper, "bitmapHeight", cue.bitmapHeight, Cue.DIMEN_UNSET); + if (cue.windowColorSet) { + dumper.add("cue.windowColor", cue.windowColor); + } + dumpIfNotEqual(dumper, "textSizeType", cue.textSizeType, Cue.TYPE_UNSET); + dumpIfNotEqual(dumper, "textSize", cue.textSize, Cue.DIMEN_UNSET); + dumpIfNotEqual(dumper, "verticalType", cue.verticalType, Cue.TYPE_UNSET); + dumper.endBlock(); + } + dumper.endBlock(); + } + dumper.endBlock(); + } + + private static void dumpIfNotEqual( + Dumper dumper, String field, @Nullable Object actual, @Nullable Object comparison) { + if (!Util.areEqual(actual, comparison)) { + dumper.add(field, actual); + } + } + + private static void dumpBitmap(Dumper dumper, @Nullable Bitmap bitmap) { + if (bitmap == null) { + return; + } + byte[] bytes = new byte[bitmap.getByteCount()]; + bitmap.copyPixelsToBuffer(ByteBuffer.wrap(bytes)); + dumper.add("bitmap", bytes); + } } diff --git a/testdata/src/test/assets/media/dash/webvtt-in-mp4/sample.mpd b/testdata/src/test/assets/media/dash/webvtt-in-mp4/sample.mpd new file mode 100644 index 0000000000..fae0dc98ec --- /dev/null +++ b/testdata/src/test/assets/media/dash/webvtt-in-mp4/sample.mpd @@ -0,0 +1,23 @@ + + + + + + + + sample.text.mp4 + + + + + + + + sample.video.mp4 + + + + + + + diff --git a/testdata/src/test/assets/media/dash/webvtt-in-mp4/sample.text.mp4 b/testdata/src/test/assets/media/dash/webvtt-in-mp4/sample.text.mp4 new file mode 100644 index 0000000000000000000000000000000000000000..44844168f87ef92b18d9857b103ac5f29818712d GIT binary patch literal 1006 zcmZuv&2G~`5S|7kI8-QyRz-+|1-&&tN*bwEFH|i^g8#`dNrVdT|(-?uZfyW;_XAi|3*aj5|^@`olH4Ww{!Xrd6H z_1UCpL0j4svE-N^AHVEl=AxU4a4u%bLNV-( zdV_9wus;|M4!TxpC7hzCy+JGHRO|)L%Ld%IY5+fd-AZwFXN4dCleTfk$V8A782SGu zKl@T9behdTb&kz46XK7>LTY*q*;kV+BJ#Vk@^__G%-Q%ho4M6Njo&+*YQiTDUHOx1 zefX+Ce+K8=B6|A#(aG6lLTszhV(QY|hUz6D#TD8P(UJE?+|d{j2TZzJn1xJTHZ#hGo4r0)*%5!!q~XK@S<;#*pLr#)KyzMHl00BnEq+(^MS x_NcSmr~8?>(33cE0eu*&AWE#mz~xg+P^%uNQ78>2bn2Ad)fA_~bRpMs{sO!7u+9Jg literal 0 HcmV?d00001 diff --git a/testdata/src/test/assets/media/dash/webvtt-in-mp4/sample.video.mp4 b/testdata/src/test/assets/media/dash/webvtt-in-mp4/sample.video.mp4 new file mode 100644 index 0000000000000000000000000000000000000000..7f456f85f90287364aa84b1ff6d39e31137ee8aa GIT binary patch literal 91242 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 literal 0 HcmV?d00001 diff --git a/testdata/src/test/assets/playbackdumps/dash/webvtt-in-mp4.dump b/testdata/src/test/assets/playbackdumps/dash/webvtt-in-mp4.dump new file mode 100644 index 0000000000..3fbd68f8db --- /dev/null +++ b/testdata/src/test/assets/playbackdumps/dash/webvtt-in-mp4.dump @@ -0,0 +1,58 @@ +MediaCodec (video/avc): + buffers.length = 31 + buffers[0] = length 36692, hash D216076E + buffers[1] = length 5312, hash D45D3CA0 + buffers[2] = length 599, hash 1BE7812D + buffers[3] = length 7735, hash 4490F110 + buffers[4] = length 987, hash 560B5036 + buffers[5] = length 673, hash ED7CD8C7 + buffers[6] = length 523, hash 3020DF50 + buffers[7] = length 6061, hash 736C72B2 + buffers[8] = length 992, hash FE132F23 + buffers[9] = length 623, hash 5B2C1816 + buffers[10] = length 421, hash 742E69C1 + buffers[11] = length 4899, hash F72F86A1 + buffers[12] = length 568, hash 519A8E50 + buffers[13] = length 620, hash 3990AA39 + buffers[14] = length 5450, hash F06EC4AA + buffers[15] = length 1051, hash 92DFA63A + buffers[16] = length 874, hash 69587FB4 + buffers[17] = length 781, hash 36BE495B + buffers[18] = length 4725, hash AC0C8CD3 + buffers[19] = length 1022, hash 5D8BFF34 + buffers[20] = length 790, hash 99413A99 + buffers[21] = length 610, hash 5E129290 + buffers[22] = length 2751, hash 769974CB + buffers[23] = length 745, hash B78A477A + buffers[24] = length 621, hash CF741E7A + buffers[25] = length 505, hash 1DB4894E + buffers[26] = length 1268, hash C15348DC + buffers[27] = length 880, hash C2DE85D0 + buffers[28] = length 530, hash C98BC6A8 + buffers[29] = length 568, hash 4FE5C8EA + buffers[30] = length 0, hash 1 +TextOutput: + Subtitle[0]: + Cues = [] + Subtitle[1]: + Cue[0]: + text = This is the first subtitle. + textAlignment = ALIGN_CENTER + lineType = 1 + lineAnchor = 0 + position = 0.5 + positionAnchor = 1 + size = 1.0 + Subtitle[2]: + Cues = [] + Subtitle[3]: + Cue[0]: + text = This is the second subtitle. + textAlignment = ALIGN_CENTER + lineType = 1 + lineAnchor = 0 + position = 0.5 + positionAnchor = 1 + size = 1.0 + Subtitle[4]: + Cues = [] From 13d886022107965bdac7bd3680378a2bca45fc60 Mon Sep 17 00:00:00 2001 From: ibaker Date: Thu, 1 Oct 2020 11:08:36 +0100 Subject: [PATCH 076/110] Tweak null-checking in TextRenderer#getNextEventTime() `subtitle` is only guaranteed to be non-null if `nextSubtitleEventIndex != C.INDEX_UNSET`. The null check added in https://github.com/google/ExoPlayer/commit/0efec5f6c12a5d583f24c122fbcbc1b1eebbabc3 was too early. Issue: #8017 PiperOrigin-RevId: 334777742 --- RELEASENOTES.md | 2 ++ .../com/google/android/exoplayer2/text/TextRenderer.java | 9 ++++++--- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 53e4e257e7..cb279f5cd1 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -13,6 +13,8 @@ * Add support for `\h` SSA/ASS style override code (non-breaking space). * Fix WebVTT subtitles in MP4 containers in DASH streams ([#7985](https://github.com/google/ExoPlayer/issues/7985)). + * Fix NPE in `TextRenderer` when playing content with a single subtitle + buffer ([#8017](https://github.com/google/ExoPlayer/issues/8017)). * UI: * Do not require subtitleButton in custom layouts of StyledPlayerView ([#7962](https://github.com/google/ExoPlayer/issues/7962)). diff --git a/library/core/src/main/java/com/google/android/exoplayer2/text/TextRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/text/TextRenderer.java index 6c140c74d1..76c1360045 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/text/TextRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/text/TextRenderer.java @@ -325,10 +325,13 @@ public final class TextRenderer extends BaseRenderer implements Callback { } private long getNextEventTime() { + if (nextSubtitleEventIndex == C.INDEX_UNSET) { + return Long.MAX_VALUE; + } checkNotNull(subtitle); - return nextSubtitleEventIndex == C.INDEX_UNSET - || nextSubtitleEventIndex >= subtitle.getEventTimeCount() - ? Long.MAX_VALUE : subtitle.getEventTime(nextSubtitleEventIndex); + return nextSubtitleEventIndex >= subtitle.getEventTimeCount() + ? Long.MAX_VALUE + : subtitle.getEventTime(nextSubtitleEventIndex); } private void updateOutput(List cues) { From 88abe26ec38ddeb20cb353e8e75d97d65a2c4629 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Fri, 2 Oct 2020 14:03:37 +0100 Subject: [PATCH 077/110] Allow apps to add a VideoAdPlayerCallback Issue: #7944 PiperOrigin-RevId: 335012643 --- RELEASENOTES.md | 2 ++ .../exoplayer2/ext/ima/ImaAdsLoader.java | 20 +++++++++++++++++++ 2 files changed, 22 insertions(+) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index cb279f5cd1..3b2aa2e158 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -36,6 +36,8 @@ * IMA extension: * Fix position reporting after fetch errors ([#7956](https://github.com/google/ExoPlayer/issues/7956)). + * Allow apps to specify a `VideoAdPlayerCallback` + ([#7944](https://github.com/google/ExoPlayer/issues/7944)). ### 2.12.0 (2020-09-11) ### diff --git a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java index 592920bfc4..ffece0f110 100644 --- a/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java +++ b/extensions/ima/src/main/java/com/google/android/exoplayer2/ext/ima/ImaAdsLoader.java @@ -125,6 +125,7 @@ public final class ImaAdsLoader @Nullable private ImaSdkSettings imaSdkSettings; @Nullable private AdErrorListener adErrorListener; @Nullable private AdEventListener adEventListener; + @Nullable private VideoAdPlayer.VideoAdPlayerCallback videoAdPlayerCallback; @Nullable private Set adUiElements; @Nullable private Collection companionAdSlots; private long adPreloadTimeoutMs; @@ -190,6 +191,22 @@ public final class ImaAdsLoader return this; } + /** + * Sets a callback to receive video ad player events. Note that these events are handled + * internally by the IMA SDK and this ads loader. For analytics and diagnostics, new + * implementations should generally use events from the top-level {@link Player} listeners + * instead of setting a callback via this method. + * + * @param videoAdPlayerCallback The callback to receive video ad player events. + * @return This builder, for convenience. + * @see com.google.ads.interactivemedia.v3.api.player.VideoAdPlayer.VideoAdPlayerCallback + */ + public Builder setVideoAdPlayerCallback( + VideoAdPlayer.VideoAdPlayerCallback videoAdPlayerCallback) { + this.videoAdPlayerCallback = checkNotNull(videoAdPlayerCallback); + return this; + } + /** * Sets the ad UI elements to be rendered by the IMA SDK. * @@ -524,6 +541,9 @@ public final class ImaAdsLoader handler = Util.createHandler(getImaLooper(), /* callback= */ null); componentListener = new ComponentListener(); adCallbacks = new ArrayList<>(/* initialCapacity= */ 1); + if (builder.videoAdPlayerCallback != null) { + adCallbacks.add(builder.videoAdPlayerCallback); + } updateAdProgressRunnable = this::updateAdProgress; adInfoByAdMediaInfo = HashBiMap.create(); supportedMimeTypes = Collections.emptyList(); From 1cca9ffd01c4b217eea77d5e7239a5921b72f92f Mon Sep 17 00:00:00 2001 From: kimvde Date: Tue, 6 Oct 2020 11:53:02 +0100 Subject: [PATCH 078/110] Add search bytes parameter to TsExtractor Context: Issue: #7988 PiperOrigin-RevId: 335608610 --- RELEASENOTES.md | 8 +-- .../extractor/DefaultExtractorsFactory.java | 20 +++++++- .../extractor/ts/TsBinarySearchSeeker.java | 18 ++++--- .../extractor/ts/TsDurationReader.java | 10 ++-- .../exoplayer2/extractor/ts/TsExtractor.java | 51 ++++++++++++++++--- .../extractor/ts/TsDurationReaderTest.java | 2 +- 6 files changed, 85 insertions(+), 24 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 3b2aa2e158..bffb735a47 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -15,9 +15,6 @@ ([#7985](https://github.com/google/ExoPlayer/issues/7985)). * Fix NPE in `TextRenderer` when playing content with a single subtitle buffer ([#8017](https://github.com/google/ExoPlayer/issues/8017)). -* UI: - * Do not require subtitleButton in custom layouts of StyledPlayerView - ([#7962](https://github.com/google/ExoPlayer/issues/7962)). * Audio: * Fix the default audio sink position not advancing correctly when using `AudioTrack`-based speed adjustment @@ -29,7 +26,12 @@ ([#7949](https://github.com/google/ExoPlayer/issues/7949)). * Fix regression for Ogg files with packets that span multiple pages ([#7992](https://github.com/google/ExoPlayer/issues/7992)). + * Add TS extractor parameter to configure the number of bytes in which + to search for a timestamp to determine the duration and to seek. + ([#7988](https://github.com/google/ExoPlayer/issues/7988)). * UI + * Do not require subtitleButton in custom layouts of StyledPlayerView + ([#7962](https://github.com/google/ExoPlayer/issues/7962)). * Add the option to sort tracks by `Format` in `TrackSelectionView` and `TrackSelectionDialogBuilder` ([#7709](https://github.com/google/ExoPlayer/issues/7709)). diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/DefaultExtractorsFactory.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/DefaultExtractorsFactory.java index 2eba1b1cca..2068853d9e 100644 --- a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/DefaultExtractorsFactory.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/DefaultExtractorsFactory.java @@ -131,9 +131,11 @@ public final class DefaultExtractorsFactory implements ExtractorsFactory { @Mp3Extractor.Flags private int mp3Flags; @TsExtractor.Mode private int tsMode; @DefaultTsPayloadReaderFactory.Flags private int tsFlags; + private int tsTimestampSearchBytes; public DefaultExtractorsFactory() { tsMode = TsExtractor.MODE_SINGLE_PMT; + tsTimestampSearchBytes = TsExtractor.DEFAULT_TIMESTAMP_SEARCH_BYTES; } /** @@ -246,7 +248,7 @@ public final class DefaultExtractorsFactory implements ExtractorsFactory { /** * Sets the mode for {@link TsExtractor} instances created by the factory. * - * @see TsExtractor#TsExtractor(int, TimestampAdjuster, TsPayloadReader.Factory) + * @see TsExtractor#TsExtractor(int, TimestampAdjuster, TsPayloadReader.Factory, int) * @param mode The mode to use. * @return The factory, for convenience. */ @@ -269,6 +271,20 @@ public final class DefaultExtractorsFactory implements ExtractorsFactory { return this; } + /** + * Sets the number of bytes searched to find a timestamp for {@link TsExtractor} instances created + * by the factory. + * + * @see TsExtractor#TsExtractor(int, TimestampAdjuster, TsPayloadReader.Factory, int) + * @param timestampSearchBytes The number of search bytes to use. + * @return The factory, for convenience. + */ + public synchronized DefaultExtractorsFactory setTsExtractorTimestampSearchBytes( + int timestampSearchBytes) { + tsTimestampSearchBytes = timestampSearchBytes; + return this; + } + @Override public synchronized Extractor[] createExtractors() { return createExtractors(Uri.EMPTY, new HashMap<>()); @@ -361,7 +377,7 @@ public final class DefaultExtractorsFactory implements ExtractorsFactory { extractors.add(new PsExtractor()); break; case FileTypes.TS: - extractors.add(new TsExtractor(tsMode, tsFlags)); + extractors.add(new TsExtractor(tsMode, tsFlags, tsTimestampSearchBytes)); break; case FileTypes.WAV: extractors.add(new WavExtractor()); diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ts/TsBinarySearchSeeker.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ts/TsBinarySearchSeeker.java index 8286189780..fa9792079c 100644 --- a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ts/TsBinarySearchSeeker.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ts/TsBinarySearchSeeker.java @@ -37,13 +37,16 @@ import java.io.IOException; private static final long SEEK_TOLERANCE_US = 100_000; private static final int MINIMUM_SEARCH_RANGE_BYTES = 5 * TsExtractor.TS_PACKET_SIZE; - private static final int TIMESTAMP_SEARCH_BYTES = 600 * TsExtractor.TS_PACKET_SIZE; public TsBinarySearchSeeker( - TimestampAdjuster pcrTimestampAdjuster, long streamDurationUs, long inputLength, int pcrPid) { + TimestampAdjuster pcrTimestampAdjuster, + long streamDurationUs, + long inputLength, + int pcrPid, + int timestampSearchBytes) { super( new DefaultSeekTimestampConverter(), - new TsPcrSeeker(pcrPid, pcrTimestampAdjuster), + new TsPcrSeeker(pcrPid, pcrTimestampAdjuster, timestampSearchBytes), streamDurationUs, /* floorTimePosition= */ 0, /* ceilingTimePosition= */ streamDurationUs + 1, @@ -58,7 +61,7 @@ import java.io.IOException; * position in a TS stream. * *

    Given a PCR timestamp, and a position within a TS stream, this seeker will peek up to {@link - * #TIMESTAMP_SEARCH_BYTES} from that stream position, look for all packets with PID equal to + * #timestampSearchBytes} from that stream position, look for all packets with PID equal to * PCR_PID, and then compare the PCR timestamps (if available) of these packets to the target * timestamp. */ @@ -67,10 +70,13 @@ import java.io.IOException; private final TimestampAdjuster pcrTimestampAdjuster; private final ParsableByteArray packetBuffer; private final int pcrPid; + private final int timestampSearchBytes; - public TsPcrSeeker(int pcrPid, TimestampAdjuster pcrTimestampAdjuster) { + public TsPcrSeeker( + int pcrPid, TimestampAdjuster pcrTimestampAdjuster, int timestampSearchBytes) { this.pcrPid = pcrPid; this.pcrTimestampAdjuster = pcrTimestampAdjuster; + this.timestampSearchBytes = timestampSearchBytes; packetBuffer = new ParsableByteArray(); } @@ -78,7 +84,7 @@ import java.io.IOException; public TimestampSearchResult searchForTimestamp(ExtractorInput input, long targetTimestamp) throws IOException { long inputPosition = input.getPosition(); - int bytesToSearch = (int) min(TIMESTAMP_SEARCH_BYTES, input.getLength() - inputPosition); + int bytesToSearch = (int) min(timestampSearchBytes, input.getLength() - inputPosition); packetBuffer.reset(bytesToSearch); input.peekFully(packetBuffer.getData(), /* offset= */ 0, bytesToSearch); diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ts/TsDurationReader.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ts/TsDurationReader.java index 5020f4c76d..504b84d575 100644 --- a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ts/TsDurationReader.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ts/TsDurationReader.java @@ -38,8 +38,7 @@ import java.io.IOException; */ /* package */ final class TsDurationReader { - private static final int TIMESTAMP_SEARCH_BYTES = 600 * TsExtractor.TS_PACKET_SIZE; - + private final int timestampSearchBytes; private final TimestampAdjuster pcrTimestampAdjuster; private final ParsableByteArray packetBuffer; @@ -51,7 +50,8 @@ import java.io.IOException; private long lastPcrValue; private long durationUs; - /* package */ TsDurationReader() { + /* package */ TsDurationReader(int timestampSearchBytes) { + this.timestampSearchBytes = timestampSearchBytes; pcrTimestampAdjuster = new TimestampAdjuster(/* firstSampleTimestampUs= */ 0); firstPcrValue = C.TIME_UNSET; lastPcrValue = C.TIME_UNSET; @@ -125,7 +125,7 @@ import java.io.IOException; private int readFirstPcrValue(ExtractorInput input, PositionHolder seekPositionHolder, int pcrPid) throws IOException { - int bytesToSearch = (int) min(TIMESTAMP_SEARCH_BYTES, input.getLength()); + int bytesToSearch = (int) min(timestampSearchBytes, input.getLength()); int searchStartPosition = 0; if (input.getPosition() != searchStartPosition) { seekPositionHolder.position = searchStartPosition; @@ -161,7 +161,7 @@ import java.io.IOException; private int readLastPcrValue(ExtractorInput input, PositionHolder seekPositionHolder, int pcrPid) throws IOException { long inputLength = input.getLength(); - int bytesToSearch = (int) min(TIMESTAMP_SEARCH_BYTES, inputLength); + int bytesToSearch = (int) min(timestampSearchBytes, inputLength); long searchStartPosition = inputLength - bytesToSearch; if (input.getPosition() != searchStartPosition) { seekPositionHolder.position = searchStartPosition; diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ts/TsExtractor.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ts/TsExtractor.java index 2fcfd422a0..2a9613f7f4 100644 --- a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ts/TsExtractor.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ts/TsExtractor.java @@ -80,6 +80,9 @@ public final class TsExtractor implements Extractor { */ public static final int MODE_HLS = 2; + public static final int TS_PACKET_SIZE = 188; + public static final int DEFAULT_TIMESTAMP_SEARCH_BYTES = 600 * TS_PACKET_SIZE; + public static final int TS_STREAM_TYPE_MPA = 0x03; public static final int TS_STREAM_TYPE_MPA_LSF = 0x04; public static final int TS_STREAM_TYPE_AAC_ADTS = 0x0F; @@ -100,7 +103,6 @@ public final class TsExtractor implements Extractor { // Stream types that aren't defined by the MPEG-2 TS specification. public static final int TS_STREAM_TYPE_AIT = 0x101; - public static final int TS_PACKET_SIZE = 188; public static final int TS_SYNC_BYTE = 0x47; // First byte of each TS packet. private static final int TS_PAT_PID = 0; @@ -115,6 +117,7 @@ public final class TsExtractor implements Extractor { private static final int SNIFF_TS_PACKET_COUNT = 5; private final @Mode int mode; + private final int timestampSearchBytes; private final List timestampAdjusters; private final ParsableByteArray tsPacketBuffer; private final SparseIntArray continuityCounters; @@ -136,7 +139,7 @@ public final class TsExtractor implements Extractor { private int pcrPid; public TsExtractor() { - this(0); + this(/* defaultTsPayloadReaderFlags= */ 0); } /** @@ -144,7 +147,7 @@ public final class TsExtractor implements Extractor { * {@code FLAG_*} values that control the behavior of the payload readers. */ public TsExtractor(@Flags int defaultTsPayloadReaderFlags) { - this(MODE_SINGLE_PMT, defaultTsPayloadReaderFlags); + this(MODE_SINGLE_PMT, defaultTsPayloadReaderFlags, DEFAULT_TIMESTAMP_SEARCH_BYTES); } /** @@ -152,12 +155,22 @@ public final class TsExtractor implements Extractor { * and {@link #MODE_HLS}. * @param defaultTsPayloadReaderFlags A combination of {@link DefaultTsPayloadReaderFactory} * {@code FLAG_*} values that control the behavior of the payload readers. + * @param timestampSearchBytes The number of bytes searched from a given position in the stream to + * find a PCR timestamp. If this value is too small, the duration might be unknown and seeking + * might not be supported for high bitrate progressive streams. Setting a large value for this + * field might be inefficient though because the extractor stores a buffer of {@code + * timestampSearchBytes} bytes when determining the duration or when performing a seek + * operation. The default value is {@link #DEFAULT_TIMESTAMP_SEARCH_BYTES}. If the number of + * bytes left in the stream from the current position is less than {@code + * timestampSearchBytes}, the search is performed on the bytes left. */ - public TsExtractor(@Mode int mode, @Flags int defaultTsPayloadReaderFlags) { + public TsExtractor( + @Mode int mode, @Flags int defaultTsPayloadReaderFlags, int timestampSearchBytes) { this( mode, new TimestampAdjuster(0), - new DefaultTsPayloadReaderFactory(defaultTsPayloadReaderFlags)); + new DefaultTsPayloadReaderFactory(defaultTsPayloadReaderFlags), + timestampSearchBytes); } /** @@ -170,7 +183,30 @@ public final class TsExtractor implements Extractor { @Mode int mode, TimestampAdjuster timestampAdjuster, TsPayloadReader.Factory payloadReaderFactory) { + this(mode, timestampAdjuster, payloadReaderFactory, DEFAULT_TIMESTAMP_SEARCH_BYTES); + } + + /** + * @param mode Mode for the extractor. One of {@link #MODE_MULTI_PMT}, {@link #MODE_SINGLE_PMT} + * and {@link #MODE_HLS}. + * @param timestampAdjuster A timestamp adjuster for offsetting and scaling sample timestamps. + * @param payloadReaderFactory Factory for injecting a custom set of payload readers. + * @param timestampSearchBytes The number of bytes searched from a given position in the stream to + * find a PCR timestamp. If this value is too small, the duration might be unknown and seeking + * might not be supported for high bitrate progressive streams. Setting a large value for this + * field might be inefficient though because the extractor stores a buffer of {@code + * timestampSearchBytes} bytes when determining the duration or when performing a seek + * operation. The default value is {@link #DEFAULT_TIMESTAMP_SEARCH_BYTES}. If the number of + * bytes left in the stream from the current position is less than {@code + * timestampSearchBytes}, the search is performed on the bytes left. + */ + public TsExtractor( + @Mode int mode, + TimestampAdjuster timestampAdjuster, + TsPayloadReader.Factory payloadReaderFactory, + int timestampSearchBytes) { this.payloadReaderFactory = Assertions.checkNotNull(payloadReaderFactory); + this.timestampSearchBytes = timestampSearchBytes; this.mode = mode; if (mode == MODE_SINGLE_PMT || mode == MODE_HLS) { timestampAdjusters = Collections.singletonList(timestampAdjuster); @@ -183,7 +219,7 @@ public final class TsExtractor implements Extractor { trackPids = new SparseBooleanArray(); tsPayloadReaders = new SparseArray<>(); continuityCounters = new SparseIntArray(); - durationReader = new TsDurationReader(); + durationReader = new TsDurationReader(timestampSearchBytes); pcrPid = -1; resetPayloadReaders(); } @@ -365,7 +401,8 @@ public final class TsExtractor implements Extractor { durationReader.getPcrTimestampAdjuster(), durationReader.getDurationUs(), inputLength, - pcrPid); + pcrPid, + timestampSearchBytes); output.seekMap(tsBinarySearchSeeker.getSeekMap()); } else { output.seekMap(new SeekMap.Unseekable(durationReader.getDurationUs())); diff --git a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/ts/TsDurationReaderTest.java b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/ts/TsDurationReaderTest.java index 8f744e855d..0e55d292b8 100644 --- a/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/ts/TsDurationReaderTest.java +++ b/library/extractor/src/test/java/com/google/android/exoplayer2/extractor/ts/TsDurationReaderTest.java @@ -37,7 +37,7 @@ public final class TsDurationReaderTest { @Before public void setUp() { - tsDurationReader = new TsDurationReader(); + tsDurationReader = new TsDurationReader(TsExtractor.DEFAULT_TIMESTAMP_SEARCH_BYTES); seekPositionHolder = new PositionHolder(); } From bddaaf022ca5584936a3d913eb091d10e5288b9a Mon Sep 17 00:00:00 2001 From: kimvde Date: Tue, 6 Oct 2020 14:24:37 +0100 Subject: [PATCH 079/110] Ignore negative payload size in TS PesReader Issue: #8005 PiperOrigin-RevId: 335625992 --- RELEASENOTES.md | 2 ++ .../exoplayer2/extractor/ts/PesReader.java | 22 +++++++++++-------- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index bffb735a47..2a00a22b56 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -29,6 +29,8 @@ * Add TS extractor parameter to configure the number of bytes in which to search for a timestamp to determine the duration and to seek. ([#7988](https://github.com/google/ExoPlayer/issues/7988)). + * Ignore negative payload size in PES packets + ([#8005](https://github.com/google/ExoPlayer/issues/8005)). * UI * Do not require subtitleButton in custom layouts of StyledPlayerView ([#7962](https://github.com/google/ExoPlayer/issues/7962)). diff --git a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ts/PesReader.java b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ts/PesReader.java index 0764087b59..97fe7a7336 100644 --- a/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ts/PesReader.java +++ b/library/extractor/src/main/java/com/google/android/exoplayer2/extractor/ts/PesReader.java @@ -97,11 +97,11 @@ public final class PesReader implements TsPayloadReader { Log.w(TAG, "Unexpected start indicator reading extended header"); break; case STATE_READING_BODY: - // If payloadSize == -1 then the length of the previous packet was unspecified, and so - // we only know that it's finished now that we've seen the start of the next one. This - // is expected. If payloadSize != -1, then the length of the previous packet was known, - // but we didn't receive that amount of data. This is not expected. - if (payloadSize != -1) { + // If payloadSize is unset then the length of the previous packet was unspecified, and so + // we only know that it's finished now that we've seen the start of the next one. This is + // expected. If payloadSize is set, then the length of the previous packet was known, but + // we didn't receive that amount of data. This is not expected. + if (payloadSize != C.LENGTH_UNSET) { Log.w(TAG, "Unexpected start indicator: expected " + payloadSize + " more bytes"); } // Either way, notify the reader that it has now finished. @@ -136,13 +136,13 @@ public final class PesReader implements TsPayloadReader { break; case STATE_READING_BODY: readLength = data.bytesLeft(); - int padding = payloadSize == -1 ? 0 : readLength - payloadSize; + int padding = payloadSize == C.LENGTH_UNSET ? 0 : readLength - payloadSize; if (padding > 0) { readLength -= padding; data.setLimit(data.getPosition() + readLength); } reader.consume(data); - if (payloadSize != -1) { + if (payloadSize != C.LENGTH_UNSET) { payloadSize -= readLength; if (payloadSize == 0) { reader.packetFinished(); @@ -191,7 +191,7 @@ public final class PesReader implements TsPayloadReader { int startCodePrefix = pesScratch.readBits(24); if (startCodePrefix != 0x000001) { Log.w(TAG, "Unexpected start code prefix: " + startCodePrefix); - payloadSize = -1; + payloadSize = C.LENGTH_UNSET; return false; } @@ -208,10 +208,14 @@ public final class PesReader implements TsPayloadReader { extendedHeaderLength = pesScratch.readBits(8); if (packetLength == 0) { - payloadSize = -1; + payloadSize = C.LENGTH_UNSET; } else { payloadSize = packetLength + 6 /* packetLength does not include the first 6 bytes */ - HEADER_SIZE - extendedHeaderLength; + if (payloadSize < 0) { + Log.w(TAG, "Found negative packet payload size: " + payloadSize); + payloadSize = C.LENGTH_UNSET; + } } return true; } From 837cdc4f67eb275ca40d99bc403d779834f0c1e7 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Tue, 6 Oct 2020 15:20:54 +0000 Subject: [PATCH 080/110] Fix miscellaneous nits/typos PiperOrigin-RevId: 335642909 --- .../google/android/exoplayer2/demo/PlayerActivity.java | 2 +- .../exoplayer2/ext/media2/DefaultMediaItemConverter.java | 2 +- .../android/exoplayer2/ext/media2/MediaItemConverter.java | 4 ++-- .../java/com/google/android/exoplayer2/ExoPlayerImpl.java | 2 +- .../com/google/android/exoplayer2/MediaPeriodQueue.java | 8 ++++---- .../android/exoplayer2/source/ProgressiveMediaSource.java | 2 +- .../exoplayer2/trackselection/MappingTrackSelector.java | 5 +++-- .../android/exoplayer2/trackselection/TrackSelector.java | 2 +- .../exoplayer2/trackselection/TrackSelectorResult.java | 7 ++++--- .../exoplayer2/trackselection/TrackSelectorTest.java | 3 ++- .../exoplayer2/upstream/DefaultBandwidthMeterTest.java | 2 +- .../exoplayer2/video/MediaCodecVideoRendererTest.java | 4 +--- .../com/google/android/exoplayer2/ui/SubtitleView.java | 1 - 13 files changed, 22 insertions(+), 22 deletions(-) diff --git a/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java b/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java index eae302887e..8fb92ed270 100644 --- a/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java +++ b/demos/main/src/main/java/com/google/android/exoplayer2/demo/PlayerActivity.java @@ -102,7 +102,7 @@ public class PlayerActivity extends AppCompatActivity private int startWindow; private long startPosition; - // Fields used only for ad playback. The ads loader is loaded via reflection. + // Fields used only for ad playback. private AdsLoader adsLoader; private Uri loadedAdTagUri; diff --git a/extensions/media2/src/main/java/com/google/android/exoplayer2/ext/media2/DefaultMediaItemConverter.java b/extensions/media2/src/main/java/com/google/android/exoplayer2/ext/media2/DefaultMediaItemConverter.java index c23bdd5669..e6d4550d88 100644 --- a/extensions/media2/src/main/java/com/google/android/exoplayer2/ext/media2/DefaultMediaItemConverter.java +++ b/extensions/media2/src/main/java/com/google/android/exoplayer2/ext/media2/DefaultMediaItemConverter.java @@ -45,7 +45,7 @@ public class DefaultMediaItemConverter implements MediaItemConverter { if (media2MediaItem instanceof CallbackMediaItem) { throw new IllegalStateException("CallbackMediaItem isn't supported"); } - + @Nullable Uri uri = null; @Nullable String mediaId = null; @Nullable String title = null; diff --git a/extensions/media2/src/main/java/com/google/android/exoplayer2/ext/media2/MediaItemConverter.java b/extensions/media2/src/main/java/com/google/android/exoplayer2/ext/media2/MediaItemConverter.java index 218c2a737e..99b284af3c 100644 --- a/extensions/media2/src/main/java/com/google/android/exoplayer2/ext/media2/MediaItemConverter.java +++ b/extensions/media2/src/main/java/com/google/android/exoplayer2/ext/media2/MediaItemConverter.java @@ -23,13 +23,13 @@ import com.google.android.exoplayer2.MediaItem; */ public interface MediaItemConverter { /** - * Converts an {@link androidx.media2.common.MediaItem Media2 MediaItem} to an {@link MediaItem + * Converts a {@link androidx.media2.common.MediaItem Media2 MediaItem} to an {@link MediaItem * ExoPlayer MediaItem}. */ MediaItem convertToExoPlayerMediaItem(androidx.media2.common.MediaItem media2MediaItem); /** - * Converts an {@link MediaItem ExoPlayer MediaItem} to an {@link androidx.media2.common.MediaItem + * Converts an {@link MediaItem ExoPlayer MediaItem} to a {@link androidx.media2.common.MediaItem * Media2 MediaItem}. */ androidx.media2.common.MediaItem convertToMedia2MediaItem(MediaItem exoPlayerMediaItem); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java index b1f5736465..21583a4d68 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java @@ -153,7 +153,7 @@ import java.util.concurrent.TimeoutException; new TrackSelectorResult( new RendererConfiguration[renderers.length], new TrackSelection[renderers.length], - null); + /* info= */ null); period = new Timeline.Period(); maskingWindowIndex = C.INDEX_UNSET; playbackInfoUpdateHandler = new Handler(applicationLooper); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodQueue.java b/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodQueue.java index b64a9c8087..fa6201bf37 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodQueue.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/MediaPeriodQueue.java @@ -315,8 +315,8 @@ import com.google.common.collect.ImmutableList; public boolean updateQueuedPeriods( Timeline timeline, long rendererPositionUs, long maxRendererReadPositionUs) { // TODO: Merge this into setTimeline so that the queue gets updated as soon as the new timeline - // is set, once all cases handled by ExoPlayerImplInternal.handleSourceInfoRefreshed can be - // handled here. + // is set, once all cases handled by ExoPlayerImplInternal.handleMediaSourceListInfoRefreshed + // can be handled here. MediaPeriodHolder previousPeriodHolder = null; MediaPeriodHolder periodHolder = playing; while (periodHolder != null) { @@ -326,8 +326,8 @@ import com.google.common.collect.ImmutableList; MediaPeriodInfo newPeriodInfo; if (previousPeriodHolder == null) { // The id and start position of the first period have already been verified by - // ExoPlayerImplInternal.handleSourceInfoRefreshed. Just update duration, isLastInTimeline - // and isLastInPeriod flags. + // ExoPlayerImplInternal.handleMediaSourceListInfoRefreshed. Just update duration, + // isLastInTimeline and isLastInPeriod flags. newPeriodInfo = getUpdatedMediaPeriodInfo(timeline, oldPeriodInfo); } else { newPeriodInfo = diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/ProgressiveMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/ProgressiveMediaSource.java index 4d7230cc3a..19f09fde22 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/ProgressiveMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/ProgressiveMediaSource.java @@ -340,7 +340,7 @@ public final class ProgressiveMediaSource extends BaseMediaSource /* manifest= */ null, mediaItem); if (timelineIsPlaceholder) { - // TODO: Actually prepare the extractors during prepatation so that we don't need a + // TODO: Actually prepare the extractors during preparation so that we don't need a // placeholder. See https://github.com/google/ExoPlayer/issues/4727. timeline = new ForwardingTimeline(timeline) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/MappingTrackSelector.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/MappingTrackSelector.java index 9949a370ed..16c63353ee 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/MappingTrackSelector.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/MappingTrackSelector.java @@ -339,14 +339,15 @@ public abstract class MappingTrackSelector extends TrackSelector { * Returns the mapping information for the currently active track selection, or null if no * selection is currently active. */ - public final @Nullable MappedTrackInfo getCurrentMappedTrackInfo() { + @Nullable + public final MappedTrackInfo getCurrentMappedTrackInfo() { return currentMappedTrackInfo; } // TrackSelector implementation. @Override - public final void onSelectionActivated(Object info) { + public final void onSelectionActivated(@Nullable Object info) { currentMappedTrackInfo = (MappedTrackInfo) info; } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelector.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelector.java index 8ee9d29d3d..59c5d5447b 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelector.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelector.java @@ -137,7 +137,7 @@ public abstract class TrackSelector { * * @param info The value of {@link TrackSelectorResult#info} in the activated selection. */ - public abstract void onSelectionActivated(Object info); + public abstract void onSelectionActivated(@Nullable Object info); /** * Calls {@link InvalidationListener#onTrackSelectionsInvalidated()} to invalidate all previously diff --git a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelectorResult.java b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelectorResult.java index 9228f3af62..67623c2cf6 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelectorResult.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/trackselection/TrackSelectorResult.java @@ -40,19 +40,20 @@ public final class TrackSelectorResult { * An opaque object that will be returned to {@link TrackSelector#onSelectionActivated(Object)} * should the selections be activated. */ - public final Object info; + @Nullable public final Object info; /** * @param rendererConfigurations A {@link RendererConfiguration} for each renderer. A null entry * indicates the corresponding renderer should be disabled. * @param selections A {@link TrackSelectionArray} containing the selection for each renderer. * @param info An opaque object that will be returned to {@link - * TrackSelector#onSelectionActivated(Object)} should the selection be activated. + * TrackSelector#onSelectionActivated(Object)} should the selection be activated. May be + * {@code null}. */ public TrackSelectorResult( @NullableType RendererConfiguration[] rendererConfigurations, @NullableType TrackSelection[] selections, - Object info) { + @Nullable Object info) { this.rendererConfigurations = rendererConfigurations; this.selections = new TrackSelectionArray(selections); this.info = info; diff --git a/library/core/src/test/java/com/google/android/exoplayer2/trackselection/TrackSelectorTest.java b/library/core/src/test/java/com/google/android/exoplayer2/trackselection/TrackSelectorTest.java index 477f7226a4..f407331711 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/trackselection/TrackSelectorTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/trackselection/TrackSelectorTest.java @@ -18,6 +18,7 @@ package com.google.android.exoplayer2.trackselection; import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.fail; +import androidx.annotation.Nullable; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.RendererCapabilities; @@ -52,7 +53,7 @@ public class TrackSelectorTest { } @Override - public void onSelectionActivated(Object info) {} + public void onSelectionActivated(@Nullable Object info) {} }; } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/upstream/DefaultBandwidthMeterTest.java b/library/core/src/test/java/com/google/android/exoplayer2/upstream/DefaultBandwidthMeterTest.java index 23f5a17e93..0b807c487a 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/upstream/DefaultBandwidthMeterTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/upstream/DefaultBandwidthMeterTest.java @@ -107,7 +107,7 @@ public final class DefaultBandwidthMeterTest { /* isAvailable= */ true, CONNECTED); } - + @Test public void defaultInitialBitrateEstimate_forWifi_isGreaterThanEstimateFor2G() { setActiveNetworkInfo(networkInfoWifi); diff --git a/library/core/src/test/java/com/google/android/exoplayer2/video/MediaCodecVideoRendererTest.java b/library/core/src/test/java/com/google/android/exoplayer2/video/MediaCodecVideoRendererTest.java index 4ba5eb34b1..74d110516b 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/video/MediaCodecVideoRendererTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/video/MediaCodecVideoRendererTest.java @@ -46,7 +46,6 @@ import com.google.android.exoplayer2.drm.DrmSessionEventListener; import com.google.android.exoplayer2.drm.DrmSessionManager; import com.google.android.exoplayer2.mediacodec.MediaCodecInfo; import com.google.android.exoplayer2.mediacodec.MediaCodecSelector; -import com.google.android.exoplayer2.mediacodec.MediaCodecUtil.DecoderQueryException; import com.google.android.exoplayer2.testutil.FakeSampleStream; import com.google.android.exoplayer2.testutil.FakeSampleStream.FakeSampleStreamItem; import com.google.android.exoplayer2.util.MimeTypes; @@ -107,8 +106,7 @@ public class MediaCodecVideoRendererTest { /* maxDroppedFramesToNotify= */ 1) { @Override @Capabilities - protected int supportsFormat(MediaCodecSelector mediaCodecSelector, Format format) - throws DecoderQueryException { + protected int supportsFormat(MediaCodecSelector mediaCodecSelector, Format format) { return RendererCapabilities.create(FORMAT_HANDLED); } diff --git a/library/ui/src/main/java/com/google/android/exoplayer2/ui/SubtitleView.java b/library/ui/src/main/java/com/google/android/exoplayer2/ui/SubtitleView.java index 452be5a3b7..bfd18aead7 100644 --- a/library/ui/src/main/java/com/google/android/exoplayer2/ui/SubtitleView.java +++ b/library/ui/src/main/java/com/google/android/exoplayer2/ui/SubtitleView.java @@ -414,5 +414,4 @@ public final class SubtitleView extends FrameLayout implements TextOutput { return cue; } - } From 5b0b4479ae7e088d8dfa90ac8a7a043675f8fff5 Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Wed, 7 Oct 2020 10:02:42 +0100 Subject: [PATCH 081/110] Fix HLS chunkful preparation bug affecting certain master playlists The bug affects playlists that start with an I-FRAME only variant. Issue: #8025 PiperOrigin-RevId: 335819497 --- RELEASENOTES.md | 4 ++++ .../google/android/exoplayer2/source/hls/HlsChunkSource.java | 4 +++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 2a00a22b56..1f1bb38b99 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -37,6 +37,10 @@ * Add the option to sort tracks by `Format` in `TrackSelectionView` and `TrackSelectionDialogBuilder` ([#7709](https://github.com/google/ExoPlayer/issues/7709)). +* HLS: + * Fix crash affecting chunkful preparation of master playlists that start + with an I-FRAME only variant + ([#8025](https://github.com/google/ExoPlayer/issues/8025)). * IMA extension: * Fix position reporting after fetch errors ([#7956](https://github.com/google/ExoPlayer/issues/7956)). diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsChunkSource.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsChunkSource.java index 530d56fa9c..2ab4852339 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsChunkSource.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsChunkSource.java @@ -592,7 +592,9 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; public InitializationTrackSelection(TrackGroup group, int[] tracks) { super(group, tracks); - selectedIndex = indexOf(group.getFormat(0)); + // The initially selected index corresponds to the first EXT-X-STREAMINF tag in the master + // playlist. + selectedIndex = indexOf(group.getFormat(tracks[0])); } @Override From c8879392ab9665f0c06d742744f606d2cf359584 Mon Sep 17 00:00:00 2001 From: bachinger Date: Wed, 7 Oct 2020 13:30:05 +0100 Subject: [PATCH 082/110] Make resetPosition reset the position if true Issue: #8024 PiperOrigin-RevId: 335846035 --- .../exoplayer2/ext/cast/CastPlayer.java | 7 ++++ .../google/android/exoplayer2/BasePlayer.java | 6 --- .../android/exoplayer2/ExoPlayerImpl.java | 5 +++ .../android/exoplayer2/ExoPlayerTest.java | 37 +++++++++++++++++++ 4 files changed, 49 insertions(+), 6 deletions(-) diff --git a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastPlayer.java b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastPlayer.java index 80d9817a46..eeda98d2d9 100644 --- a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastPlayer.java +++ b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastPlayer.java @@ -307,6 +307,13 @@ public final class CastPlayer extends BasePlayer { } } + @Override + public void setMediaItems(List mediaItems, boolean resetPosition) { + int windowIndex = resetPosition ? 0 : getCurrentWindowIndex(); + long startPositionMs = resetPosition ? C.TIME_UNSET : getContentPosition(); + setMediaItems(mediaItems, windowIndex, startPositionMs); + } + @Override public void setMediaItems( List mediaItems, int startWindowIndex, long startPositionMs) { diff --git a/library/core/src/main/java/com/google/android/exoplayer2/BasePlayer.java b/library/core/src/main/java/com/google/android/exoplayer2/BasePlayer.java index 9d7af2dce6..4f89925121 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/BasePlayer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/BasePlayer.java @@ -44,12 +44,6 @@ public abstract class BasePlayer implements Player { setMediaItems(Collections.singletonList(mediaItem), resetPosition); } - @Override - public void setMediaItems(List mediaItems, boolean resetPosition) { - setMediaItems( - mediaItems, /* startWindowIndex= */ C.INDEX_UNSET, /* startPositionMs= */ C.TIME_UNSET); - } - @Override public void setMediaItems(List mediaItems) { setMediaItems(mediaItems, /* resetPosition= */ true); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java index 21583a4d68..377863a083 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayerImpl.java @@ -347,6 +347,11 @@ import java.util.concurrent.TimeoutException; prepare(); } + @Override + public void setMediaItems(List mediaItems, boolean resetPosition) { + setMediaSources(createMediaSources(mediaItems), resetPosition); + } + @Override public void setMediaItems( List mediaItems, int startWindowIndex, long startPositionMs) { diff --git a/library/core/src/test/java/com/google/android/exoplayer2/ExoPlayerTest.java b/library/core/src/test/java/com/google/android/exoplayer2/ExoPlayerTest.java index 7934298df0..ffd46f9089 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/ExoPlayerTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/ExoPlayerTest.java @@ -104,6 +104,7 @@ import com.google.android.exoplayer2.upstream.TransferListener; import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Clock; import com.google.common.collect.ImmutableList; +import com.google.common.collect.Lists; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; @@ -5699,6 +5700,42 @@ public final class ExoPlayerTest { assertArrayEquals(new int[] {0, 0, 0}, currentWindowIndices); } + @Test + public void setMediaItems_resetPosition_resetsPosition() throws Exception { + final int[] currentWindowIndices = {C.INDEX_UNSET, C.INDEX_UNSET}; + final long[] currentPositions = {C.INDEX_UNSET, C.INDEX_UNSET}; + ActionSchedule actionSchedule = + new ActionSchedule.Builder(TAG) + .pause() + .executeRunnable( + new PlayerRunnable() { + @Override + public void run(SimpleExoPlayer player) { + player.seekTo(/* windowIndex= */ 1, /* positionMs= */ 1000); + currentWindowIndices[0] = player.getCurrentWindowIndex(); + currentPositions[0] = player.getCurrentPosition(); + List listOfTwo = + Lists.newArrayList( + MediaItem.fromUri(Uri.EMPTY), MediaItem.fromUri(Uri.EMPTY)); + player.setMediaItems(listOfTwo, /* resetPosition= */ true); + currentWindowIndices[1] = player.getCurrentWindowIndex(); + currentPositions[1] = player.getCurrentPosition(); + } + }) + .prepare() + .waitForTimelineChanged() + .play() + .build(); + new ExoPlayerTestRunner.Builder(context) + .setActionSchedule(actionSchedule) + .build() + .start(/* doPrepare= */ false) + .blockUntilActionScheduleFinished(TIMEOUT_MS) + .blockUntilEnded(TIMEOUT_MS); + assertArrayEquals(new int[] {1, 0}, currentWindowIndices); + assertArrayEquals(new long[] {1000, 0}, currentPositions); + } + @Test public void setMediaSources_empty_whenEmpty_validInitialSeek_correctMaskingWindowIndex() throws Exception { From 1e315e47d642a61a8bd33f993bab1418a91ab1b0 Mon Sep 17 00:00:00 2001 From: insun Date: Thu, 8 Oct 2020 09:58:11 +0100 Subject: [PATCH 083/110] Expand bottom button's height and extend greyed background area to seekbar Adjusted the bottom layout of StyledPlayerControlView : - Enlarged bottom button's height to make tapping easier. - Extended greyed background area to upper edge of seekbar. - Gave padding between bottom edge of the overall layout and bottom buttons. - Reduced horizontal margins between bottom buttons. PiperOrigin-RevId: 336041160 --- RELEASENOTES.md | 16 ++++++++++------ .../layout/exo_styled_player_control_view.xml | 3 ++- library/ui/src/main/res/values/dimens.xml | 15 +++++++++------ library/ui/src/main/res/values/styles.xml | 10 ++++++---- 4 files changed, 27 insertions(+), 17 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 1f1bb38b99..c31a803c5e 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -15,6 +15,16 @@ ([#7985](https://github.com/google/ExoPlayer/issues/7985)). * Fix NPE in `TextRenderer` when playing content with a single subtitle buffer ([#8017](https://github.com/google/ExoPlayer/issues/8017)). +* UI: + * Do not require subtitleButton in custom layouts of StyledPlayerView + ([#7962](https://github.com/google/ExoPlayer/issues/7962)). + * Add the option to sort tracks by `Format` in `TrackSelectionView` and + `TrackSelectionDialogBuilder` + ([#7709](https://github.com/google/ExoPlayer/issues/7709)). + * Adjusted bottom buttons' heights and paddings in StyledPlayerView for + easy tapping. + * Show overflow button in `StyledPlayerControlView` only when there is no + enough space. * Audio: * Fix the default audio sink position not advancing correctly when using `AudioTrack`-based speed adjustment @@ -31,12 +41,6 @@ ([#7988](https://github.com/google/ExoPlayer/issues/7988)). * Ignore negative payload size in PES packets ([#8005](https://github.com/google/ExoPlayer/issues/8005)). -* UI - * Do not require subtitleButton in custom layouts of StyledPlayerView - ([#7962](https://github.com/google/ExoPlayer/issues/7962)). - * Add the option to sort tracks by `Format` in `TrackSelectionView` and - `TrackSelectionDialogBuilder` - ([#7709](https://github.com/google/ExoPlayer/issues/7709)). * HLS: * Fix crash affecting chunkful preparation of master playlists that start with an I-FRAME only variant diff --git a/library/ui/src/main/res/layout/exo_styled_player_control_view.xml b/library/ui/src/main/res/layout/exo_styled_player_control_view.xml index 3136f9d811..3fb8b98ce5 100644 --- a/library/ui/src/main/res/layout/exo_styled_player_control_view.xml +++ b/library/ui/src/main/res/layout/exo_styled_player_control_view.xml @@ -40,11 +40,12 @@ android:layout_height="@dimen/exo_bottom_bar_height" android:layout_gravity="bottom" android:background="@color/exo_bottom_bar_background" + android:paddingBottom="@dimen/exo_bottom_bar_padding_bottom" android:layoutDirection="ltr"> 8dp 52dp - 5dp + 5dp 2dp 9dp 18dp - 48dp - 32dp + 48dp + 48dp + 2dp 12dp - 4dp + 12dp 2dp 24dp - 40dp + 56dp - 32dp + 70dp + 4dp 10dp 170sp + 48dp 32dp 64dp diff --git a/library/ui/src/main/res/values/styles.xml b/library/ui/src/main/res/values/styles.xml index d86c3e5a39..03afddfdc5 100644 --- a/library/ui/src/main/res/values/styles.xml +++ b/library/ui/src/main/res/values/styles.xml @@ -61,8 +61,8 @@ + From d7e8238b88e7ddfa1b7ed37729e4df7c72b41551 Mon Sep 17 00:00:00 2001 From: olly Date: Wed, 21 Oct 2020 15:50:35 +0100 Subject: [PATCH 109/110] Bump version to 2.12.1 PiperOrigin-RevId: 338261975 --- RELEASENOTES.md | 74 +++++++++---------- constants.gradle | 4 +- .../exoplayer2/ExoPlayerLibraryInfo.java | 6 +- 3 files changed, 39 insertions(+), 45 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 910781495c..19ceda9499 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -1,63 +1,57 @@ # Release notes -### 2.12.1 ### +### 2.12.1 (2020-10-23) ### * Core library: - * Fix bug where streams with highly uneven durations may get stuck in a - buffering state - ([#7943](https://github.com/google/ExoPlayer/issues/7943)). - * Switch Guava dependency from `implementation` to `api` - ([#7905](https://github.com/google/ExoPlayer/issues/7905), - ([#7993](https://github.com/google/ExoPlayer/issues/7993)). + * Fix bug where streams with highly uneven track durations may get stuck + in a buffering state + * Add 403, 500 and 503 to the list of HTTP status codes that can trigger + failover to another quality variant during adaptive playbacks. * Data sources: * Add support for `android.resource` URI scheme in `RawResourceDataSource` ([#7866](https://github.com/google/ExoPlayer/issues/7866)). * Text: * Add support for `\h` SSA/ASS style override code (non-breaking space). - * Fix WebVTT subtitles in MP4 containers in DASH streams + * Fix playback of WebVTT subtitles in MP4 containers in DASH streams ([#7985](https://github.com/google/ExoPlayer/issues/7985)). - * Fix NPE in `TextRenderer` when playing content with a single subtitle - buffer ([#8017](https://github.com/google/ExoPlayer/issues/8017)). + * Fix `NullPointerException` in `TextRenderer` when playing content with a + single subtitle buffer + ([#8017](https://github.com/google/ExoPlayer/issues/8017)). * UI: - * Show overflow button in `StyledPlayerControlView` only when there is not - enough space. * Fix animation when `StyledPlayerView` first shows its playback controls. - * Allow subtitleButton to be omitted in custom `StyledPlayerView` layouts - ([#7962](https://github.com/google/ExoPlayer/issues/7962)). + * Improve touch targets in `StyledPlayerView` to make tapping easier. + * Allow `subtitleButton` to be omitted in custom `StyledPlayerView` + layouts ([#7962](https://github.com/google/ExoPlayer/issues/7962)). * Add an option to sort tracks by `Format` in `TrackSelectionView` and `TrackSelectionDialogBuilder` ([#7709](https://github.com/google/ExoPlayer/issues/7709)). - * Improve touch targets in `StyledPlayerView` to make tapping easier. * Audio: * Fix the default audio sink position not advancing correctly when using - `AudioTrack`-based speed adjustment + `AudioTrack` based speed adjustment ([#7982](https://github.com/google/ExoPlayer/issues/7982)). * Fix `NoClassDefFoundError` warning for `AudioTrack$StreamEventCallback` - even though the class was not used ([#8058](https://github.com/google/ExoPlayer/issues/8058)). * Extractors: - * Add support for `_mp2` boxes in `Mp4Extractor` - ([#7967](https://github.com/google/ExoPlayer/issues/7967)). - * Fix playback of MP4 and MOV files containing `pcm_alaw` or `pcm_mulaw` - audio tracks, by enabling sample rechunking of such tracks - * Use TLEN ID3 tag to compute the duration in `Mp3Extractor` + * MP4: + * Add support for `_mp2` boxes + ([#7967](https://github.com/google/ExoPlayer/issues/7967)). + * Fix playback of files containing `pcm_alaw` or `pcm_mulaw` audio + tracks, by enabling sample rechunking for such tracks. + * MPEG-TS: + * Add `TsExtractor` parameter to configure the number of bytes in + which to search for timestamps when seeking and determining stream + duration ([#7988](https://github.com/google/ExoPlayer/issues/7988)). + * Ignore negative payload size in PES packets + ([#8005](https://github.com/google/ExoPlayer/issues/8005)). + * MP3: Use TLEN ID3 tag to compute the stream duration ([#7949](https://github.com/google/ExoPlayer/issues/7949)). - * Fix regression for Ogg files with packets that span multiple pages + * Ogg: Fix regression playing files with packets that span multiple pages ([#7992](https://github.com/google/ExoPlayer/issues/7992)). - * Add TS extractor parameter to configure the number of bytes in which - to search for a timestamp to determine the duration and to seek. - ([#7988](https://github.com/google/ExoPlayer/issues/7988)). - * Ignore negative payload size in PES packets - ([#8005](https://github.com/google/ExoPlayer/issues/8005)). - * Make FLV files seekable by using the key frame index + * FLV: Make files seekable by using the key frame index ([#7378](https://github.com/google/ExoPlayer/issues/7378)). -* Adaptive playback (DASH / HLS / SmoothStreaming): - * Add 403, 500 and 503 to the list of HTTP status codes that can trigger - failover to another quality variant. -* HLS: - * Fix crash affecting chunkful preparation of master playlists that start - with an I-FRAME only variant - ([#8025](https://github.com/google/ExoPlayer/issues/8025)). +* HLS: Fix crash affecting chunkful preparation of master playlists that start + with an I-FRAME only variant + ([#8025](https://github.com/google/ExoPlayer/issues/8025)). * IMA extension: * Fix position reporting after fetch errors ([#7956](https://github.com/google/ExoPlayer/issues/7956)). @@ -70,7 +64,7 @@ ([#3750](https://github.com/google/ExoPlayer/issues/3750)). * Add a way to override ad media MIME types ([#7961)(https://github.com/google/ExoPlayer/issues/7961)). - * Fix truncating large cue points in microseconds + * Fix incorrect truncation of large cue point positions ([#8067](https://github.com/google/ExoPlayer/issues/8067)). * Upgrade IMA SDK dependency to 3.20.1. This brings in a fix for companion ads rendering when targeting API 29 @@ -235,7 +229,7 @@ To learn more about what's new in 2.12, read the corresponding * Redefine `Cue.lineType=LINE_TYPE_NUMBER` in terms of aligning the cue text lines to grid of viewport lines. Only consider `Cue.lineAnchor` when `Cue.lineType=LINE_TYPE_FRACTION`. - * WebVTT + * WebVTT: * Add support for default [text](https://www.w3.org/TR/webvtt1/#default-text-color) and [background](https://www.w3.org/TR/webvtt1/#default-text-background) @@ -250,10 +244,10 @@ To learn more about what's new in 2.12, read the corresponding * Parse the `ruby-position` CSS property. * Parse the `text-combine-upright` CSS property (i.e., tate-chu-yoko). * Parse the `` and `` tags. - * TTML + * TTML: * Parse the `tts:combineText` property (i.e., tate-chu-yoko). * Parse t`tts:ruby` and `tts:rubyPosition` properties. - * CEA-608 + * CEA-608: * Implement timing-out of stuck captions, as permitted by ANSI/CTA-608-E R-2014 Annex C.9. The default timeout is set to 16 seconds ([#7181](https://github.com/google/ExoPlayer/issues/7181)). diff --git a/constants.gradle b/constants.gradle index c2b0000368..44a61d6baa 100644 --- a/constants.gradle +++ b/constants.gradle @@ -13,8 +13,8 @@ // limitations under the License. project.ext { // ExoPlayer version and version code. - releaseVersion = '2.12.0' - releaseVersionCode = 2012000 + releaseVersion = '2.12.1' + releaseVersionCode = 2012001 minSdkVersion = 16 appTargetSdkVersion = 29 targetSdkVersion = 28 // TODO: Bump once b/143232359 is resolved. Also fix TODOs in UtilTest. diff --git a/library/common/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java b/library/common/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java index 15c4bf1c1d..b751fff7bd 100644 --- a/library/common/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java +++ b/library/common/src/main/java/com/google/android/exoplayer2/ExoPlayerLibraryInfo.java @@ -30,11 +30,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.12.0"; + public static final String VERSION = "2.12.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.12.0"; + public static final String VERSION_SLASHY = "ExoPlayerLib/2.12.1"; /** * The version of the library expressed as an integer, for example 1002003. @@ -44,7 +44,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 = 2012000; + public static final int VERSION_INT = 2012001; /** The default user agent for requests made by the library. */ public static final String DEFAULT_USER_AGENT = From 269ea4ea2b46d379807cd6f0b86074d0b0fc7eae Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 22 Oct 2020 12:32:37 +0100 Subject: [PATCH 110/110] Add missing release note PiperOrigin-RevId: 338446775 --- RELEASENOTES.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 19ceda9499..29c3fcf90f 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -3,6 +3,8 @@ ### 2.12.1 (2020-10-23) ### * Core library: + * Fix issue where `Player.setMediaItems` would ignore its `resetPosition` + argument ([#8024](https://github.com/google/ExoPlayer/issues/8024)). * Fix bug where streams with highly uneven track durations may get stuck in a buffering state * Add 403, 500 and 503 to the list of HTTP status codes that can trigger