diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 322d68af8c..c70da55360 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -82,6 +82,10 @@ if playback is ongoing or stops the service otherwise. * UI: * Downloads: + * Ensure that `DownloadHelper` doesn't leak unreleased `Renderer` + instances, which can eventually result in an app crashing with + `IllegalStateException: Too many receivers, total of 1000, registered + for pid` ([#1224](https://github.com/androidx/media/issues/1224)). * OkHttp Extension: * Cronet Extension: * RTMP Extension: diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/DefaultRendererCapabilitiesList.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/DefaultRendererCapabilitiesList.java index 729581eb6a..5486324078 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/DefaultRendererCapabilitiesList.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/DefaultRendererCapabilitiesList.java @@ -57,7 +57,7 @@ public final class DefaultRendererCapabilitiesList implements RendererCapabiliti public DefaultRendererCapabilitiesList createRendererCapabilitiesList() { Renderer[] renderers = renderersFactory.createRenderers( - Util.createHandlerForCurrentLooper(), + Util.createHandlerForCurrentOrMainLooper(), new VideoRendererEventListener() {}, new AudioRendererEventListener() {}, cueGroup -> {}, @@ -84,6 +84,11 @@ public final class DefaultRendererCapabilitiesList implements RendererCapabiliti return rendererCapabilities; } + @Override + public int size() { + return renderers.length; + } + @Override public void release() { for (Renderer renderer : renderers) { diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/RendererCapabilitiesList.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/RendererCapabilitiesList.java index 4ecbde15d2..73c4f12741 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/RendererCapabilitiesList.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/RendererCapabilitiesList.java @@ -31,6 +31,9 @@ public interface RendererCapabilitiesList { /** Returns an array of {@link RendererCapabilities}. */ RendererCapabilities[] getRendererCapabilities(); + /** Returns the number of {@link RendererCapabilities}. */ + int size(); + /** Releases any resources associated with this {@link RendererCapabilitiesList}. */ void release(); } diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/offline/DownloadHelper.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/offline/DownloadHelper.java index a44a58fe5a..2c7cc36414 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/offline/DownloadHelper.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/offline/DownloadHelper.java @@ -38,10 +38,12 @@ import androidx.media3.common.util.UnstableApi; import androidx.media3.common.util.Util; import androidx.media3.datasource.DataSource; import androidx.media3.datasource.TransferListener; +import androidx.media3.exoplayer.DefaultRendererCapabilitiesList; import androidx.media3.exoplayer.ExoPlaybackException; import androidx.media3.exoplayer.LoadingInfo; import androidx.media3.exoplayer.Renderer; import androidx.media3.exoplayer.RendererCapabilities; +import androidx.media3.exoplayer.RendererCapabilitiesList; import androidx.media3.exoplayer.RenderersFactory; import androidx.media3.exoplayer.analytics.PlayerId; import androidx.media3.exoplayer.audio.AudioRendererEventListener; @@ -144,12 +146,12 @@ public final class DownloadHelper { public static class LiveContentUnsupportedException extends IOException {} /** - * Extracts renderer capabilities for the renderers created by the provided renderers factory. - * - * @param renderersFactory A {@link RenderersFactory}. - * @return The {@link RendererCapabilities} for each renderer created by the {@code - * renderersFactory}. + * @deprecated This method leaks un-released {@link Renderer} instances. There is no direct + * replacement. Equivalent functionality can be implemented by constructing the renderer + * instances, calling {@link Renderer#getCapabilities()} on each one, then releasing the + * renderers when the capabilities are no longer required. */ + @Deprecated public static RendererCapabilities[] getRendererCapabilities(RenderersFactory renderersFactory) { Renderer[] renderers = renderersFactory.createRenderers( @@ -274,8 +276,9 @@ public final class DownloadHelper { mediaItem, castNonNull(dataSourceFactory), drmSessionManager), trackSelectionParameters, renderersFactory != null - ? getRendererCapabilities(renderersFactory) - : new RendererCapabilities[0]); + ? new DefaultRendererCapabilitiesList.Factory(renderersFactory) + .createRendererCapabilitiesList() + : new UnreleaseableRendererCapabilitiesList(new RendererCapabilities[0])); } /** @@ -308,7 +311,7 @@ public final class DownloadHelper { private final MediaItem.LocalConfiguration localConfiguration; @Nullable private final MediaSource mediaSource; private final DefaultTrackSelector trackSelector; - private final RendererCapabilities[] rendererCapabilities; + private final RendererCapabilitiesList rendererCapabilities; private final SparseIntArray scratchSet; private final Handler callbackHandler; private final Timeline.Window window; @@ -322,6 +325,26 @@ public final class DownloadHelper { private List @MonotonicNonNull [][] immutableTrackSelectionsByPeriodAndRenderer; + /** + * @deprecated The {@link Renderer} instances used to produce {@code rendererCapabilities} must be + * kept alive for the lifetime of this {@code DownloadHelper} instance and then released (to + * avoid a resource leak). Use {@link DownloadHelper#DownloadHelper(MediaItem, MediaSource, + * TrackSelectionParameters, RendererCapabilitiesList)} instead to avoid needing to manually + * manage this bookkeeping. + */ + @Deprecated + public DownloadHelper( + MediaItem mediaItem, + @Nullable MediaSource mediaSource, + TrackSelectionParameters trackSelectionParameters, + RendererCapabilities[] rendererCapabilities) { + this( + mediaItem, + mediaSource, + trackSelectionParameters, + new UnreleaseableRendererCapabilitiesList(rendererCapabilities)); + } + /** * Creates download helper. * @@ -330,14 +353,14 @@ public final class DownloadHelper { * selection needs to be made. * @param trackSelectionParameters {@link TrackSelectionParameters} for selecting tracks for * downloading. - * @param rendererCapabilities The {@link RendererCapabilities} of the renderers for which tracks - * are selected. + * @param rendererCapabilities The {@link RendererCapabilitiesList} of the renderers for which + * tracks are selected. */ public DownloadHelper( MediaItem mediaItem, @Nullable MediaSource mediaSource, TrackSelectionParameters trackSelectionParameters, - RendererCapabilities[] rendererCapabilities) { + RendererCapabilitiesList rendererCapabilities) { this.localConfiguration = checkNotNull(mediaItem.localConfiguration); this.mediaSource = mediaSource; this.trackSelector = @@ -371,6 +394,7 @@ public final class DownloadHelper { mediaPreparer.release(); } trackSelector.release(); + rendererCapabilities.release(); } /** @@ -462,7 +486,7 @@ public final class DownloadHelper { */ public void clearTrackSelections(int periodIndex) { assertPreparedWithMedia(); - for (int i = 0; i < rendererCapabilities.length; i++) { + for (int i = 0; i < rendererCapabilities.size(); i++) { trackSelectionsByPeriodAndRenderer[periodIndex][i].clear(); } } @@ -521,7 +545,7 @@ public final class DownloadHelper { // Prefer highest supported bitrate for downloads. parametersBuilder.setForceHighestSupportedBitrate(true); // Disable all non-audio track types supported by the renderers. - for (RendererCapabilities capabilities : rendererCapabilities) { + for (RendererCapabilities capabilities : rendererCapabilities.getRendererCapabilities()) { @C.TrackType int trackType = capabilities.getTrackType(); parametersBuilder.setTrackTypeDisabled( trackType, /* disabled= */ trackType != C.TRACK_TYPE_AUDIO); @@ -562,7 +586,7 @@ public final class DownloadHelper { // Prefer highest supported bitrate for downloads. parametersBuilder.setForceHighestSupportedBitrate(true); // Disable all non-text track types supported by the renderers. - for (RendererCapabilities capabilities : rendererCapabilities) { + for (RendererCapabilities capabilities : rendererCapabilities.getRendererCapabilities()) { @C.TrackType int trackType = capabilities.getTrackType(); parametersBuilder.setTrackTypeDisabled( trackType, /* disabled= */ trackType != C.TRACK_TYPE_TEXT); @@ -694,7 +718,7 @@ public final class DownloadHelper { checkNotNull(mediaPreparer.mediaPeriods); checkNotNull(mediaPreparer.timeline); int periodCount = mediaPreparer.mediaPeriods.length; - int rendererCount = rendererCapabilities.length; + int rendererCount = rendererCapabilities.size(); trackSelectionsByPeriodAndRenderer = (List[][]) new List[periodCount][rendererCount]; immutableTrackSelectionsByPeriodAndRenderer = @@ -762,7 +786,7 @@ public final class DownloadHelper { private TrackSelectorResult runTrackSelection(int periodIndex) throws ExoPlaybackException { TrackSelectorResult trackSelectorResult = trackSelector.selectTracks( - rendererCapabilities, + rendererCapabilities.getRendererCapabilities(), trackGroupArrays[periodIndex], new MediaPeriodId(mediaPreparer.timeline.getUidOfPeriod(periodIndex)), mediaPreparer.timeline); @@ -1066,4 +1090,27 @@ public final class DownloadHelper { // Do nothing. } } + + private static final class UnreleaseableRendererCapabilitiesList + implements RendererCapabilitiesList { + + private final RendererCapabilities[] rendererCapabilities; + + private UnreleaseableRendererCapabilitiesList(RendererCapabilities[] rendererCapabilities) { + this.rendererCapabilities = rendererCapabilities; + } + + @Override + public RendererCapabilities[] getRendererCapabilities() { + return rendererCapabilities; + } + + @Override + public int size() { + return rendererCapabilities.length; + } + + @Override + public void release() {} + } } diff --git a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/offline/DownloadHelperTest.java b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/offline/DownloadHelperTest.java index 8d49f4f7ab..31a57f6c3e 100644 --- a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/offline/DownloadHelperTest.java +++ b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/offline/DownloadHelperTest.java @@ -29,8 +29,14 @@ import androidx.media3.common.Timeline; import androidx.media3.common.TrackGroup; import androidx.media3.common.TrackSelectionOverride; import androidx.media3.common.TrackSelectionParameters; +import androidx.media3.datasource.DefaultDataSource; +import androidx.media3.exoplayer.DefaultRendererCapabilitiesList; import androidx.media3.exoplayer.Renderer; +import androidx.media3.exoplayer.RendererCapabilities; +import androidx.media3.exoplayer.RendererCapabilitiesList; import androidx.media3.exoplayer.RenderersFactory; +import androidx.media3.exoplayer.drm.DefaultDrmSessionManager; +import androidx.media3.exoplayer.drm.HttpMediaDrmCallback; import androidx.media3.exoplayer.offline.DownloadHelper.Callback; import androidx.media3.exoplayer.source.MediaPeriod; import androidx.media3.exoplayer.source.MediaSourceEventListener.EventDispatcher; @@ -39,6 +45,7 @@ import androidx.media3.exoplayer.trackselection.DefaultTrackSelector; import androidx.media3.exoplayer.trackselection.ExoTrackSelection; import androidx.media3.exoplayer.trackselection.MappingTrackSelector.MappedTrackInfo; import androidx.media3.exoplayer.upstream.Allocator; +import androidx.media3.test.utils.FakeDataSource; import androidx.media3.test.utils.FakeMediaPeriod; import androidx.media3.test.utils.FakeMediaSource; import androidx.media3.test.utils.FakeRenderer; @@ -123,7 +130,8 @@ public class DownloadHelperTest { testMediaItem, new TestMediaSource(), DownloadHelper.DEFAULT_TRACK_SELECTOR_PARAMETERS_WITHOUT_CONTEXT, - DownloadHelper.getRendererCapabilities(renderersFactory)); + new DefaultRendererCapabilitiesList.Factory(renderersFactory) + .createRendererCapabilitiesList()); } @Test @@ -439,6 +447,191 @@ public class DownloadHelperTest { new StreamKey(/* periodIndex= */ 0, /* groupIndex= */ 2, /* streamIndex= */ 0)); } + // https://github.com/androidx/media/issues/1224 + @Test + public void prepareThenRelease_renderersReleased() throws Exception { + // We can't use this.downloadHelper because we need access to the FakeRenderer instances for + // later assertions, so we recreate a local DownloadHelper. + FakeRenderer videoRenderer = new FakeRenderer(C.TRACK_TYPE_VIDEO); + FakeRenderer audioRenderer = new FakeRenderer(C.TRACK_TYPE_AUDIO); + FakeRenderer textRenderer = new FakeRenderer(C.TRACK_TYPE_TEXT); + RenderersFactory renderersFactory = + (handler, videoListener, audioListener, metadata, text) -> + new Renderer[] {textRenderer, audioRenderer, videoRenderer}; + DownloadHelper downloadHelper = + DownloadHelper.forMediaItem( + testMediaItem, + DownloadHelper.DEFAULT_TRACK_SELECTOR_PARAMETERS_WITHOUT_CONTEXT, + renderersFactory, + new FakeDataSource.Factory()); + + prepareDownloadHelper(downloadHelper); + downloadHelper.release(); + + assertThat(videoRenderer.isReleased).isTrue(); + assertThat(audioRenderer.isReleased).isTrue(); + assertThat(textRenderer.isReleased).isTrue(); + } + + @Test + public void forMediaItem_mediaItemOnly_worksWithoutLooperThread() throws Exception { + AtomicReference exception = new AtomicReference<>(); + AtomicReference downloadHelper = new AtomicReference<>(); + Thread thread = + new Thread( + () -> { + try { + downloadHelper.set( + DownloadHelper.forMediaItem(getApplicationContext(), testMediaItem)); + } catch (Throwable e) { + exception.set(e); + } + }); + + thread.start(); + thread.join(); + + assertThat(exception.get()).isNull(); + assertThat(downloadHelper.get()).isNotNull(); + } + + // Internal b/333089854 + @Test + public void forMediaItem_withContext_worksWithoutLooperThread() throws Exception { + AtomicReference exception = new AtomicReference<>(); + AtomicReference downloadHelper = new AtomicReference<>(); + Thread thread = + new Thread( + () -> { + try { + FakeRenderer videoRenderer = new FakeRenderer(C.TRACK_TYPE_VIDEO); + RenderersFactory renderersFactory = + (handler, videoListener, audioListener, metadata, text) -> + new Renderer[] {videoRenderer}; + downloadHelper.set( + DownloadHelper.forMediaItem( + getApplicationContext(), + testMediaItem, + renderersFactory, + new FakeDataSource.Factory())); + } catch (Throwable e) { + exception.set(e); + } + }); + + thread.start(); + thread.join(); + + assertThat(exception.get()).isNull(); + assertThat(downloadHelper.get()).isNotNull(); + } + + @Test + public void forMediaItem_withTrackSelectionParams_worksWithoutLooperThread() throws Exception { + AtomicReference exception = new AtomicReference<>(); + AtomicReference downloadHelper = new AtomicReference<>(); + Thread thread = + new Thread( + () -> { + try { + FakeRenderer videoRenderer = new FakeRenderer(C.TRACK_TYPE_VIDEO); + RenderersFactory renderersFactory = + (handler, videoListener, audioListener, metadata, text) -> + new Renderer[] {videoRenderer}; + downloadHelper.set( + DownloadHelper.forMediaItem( + testMediaItem, + TrackSelectionParameters.getDefaults(getApplicationContext()), + renderersFactory, + new FakeDataSource.Factory())); + } catch (Throwable e) { + exception.set(e); + } + }); + + thread.start(); + thread.join(); + + assertThat(exception.get()).isNull(); + assertThat(downloadHelper.get()).isNotNull(); + } + + @Test + public void forMediaItem_withTrackSelectionParamsAndDrm_worksWithoutLooperThread() + throws Exception { + AtomicReference exception = new AtomicReference<>(); + AtomicReference downloadHelper = new AtomicReference<>(); + Thread thread = + new Thread( + () -> { + try { + FakeRenderer videoRenderer = new FakeRenderer(C.TRACK_TYPE_VIDEO); + RenderersFactory renderersFactory = + (handler, videoListener, audioListener, metadata, text) -> + new Renderer[] {videoRenderer}; + downloadHelper.set( + DownloadHelper.forMediaItem( + testMediaItem, + TrackSelectionParameters.getDefaults(getApplicationContext()), + renderersFactory, + new FakeDataSource.Factory(), + new DefaultDrmSessionManager.Builder() + .build( + new HttpMediaDrmCallback( + /* defaultLicenseUrl= */ null, + new DefaultDataSource.Factory(getApplicationContext()))))); + } catch (Throwable e) { + exception.set(e); + } + }); + + thread.start(); + thread.join(); + + assertThat(exception.get()).isNull(); + assertThat(downloadHelper.get()).isNotNull(); + } + + @Test + public void constructor_worksWithoutLooperThread() throws Exception { + AtomicReference exception = new AtomicReference<>(); + AtomicReference downloadHelper = new AtomicReference<>(); + Thread thread = + new Thread( + () -> { + try { + RendererCapabilitiesList emptyRendererCapabilitiesList = + new RendererCapabilitiesList() { + @Override + public RendererCapabilities[] getRendererCapabilities() { + return new RendererCapabilities[0]; + } + + @Override + public int size() { + return 0; + } + + @Override + public void release() {} + }; + downloadHelper.set( + new DownloadHelper( + testMediaItem, + new FakeMediaSource(), + TrackSelectionParameters.getDefaults(getApplicationContext()), + emptyRendererCapabilitiesList)); + } catch (Throwable e) { + exception.set(e); + } + }); + + thread.start(); + thread.join(); + + assertThat(exception.get()).isNull(); + } + private static void prepareDownloadHelper(DownloadHelper downloadHelper) throws Exception { AtomicReference prepareException = new AtomicReference<>(null); CountDownLatch preparedLatch = new CountDownLatch(1);