PiperOrigin-RevId: 622866208
This commit is contained in:
ibaker 2024-04-08 09:18:22 -07:00 committed by Copybara-Service
parent 516d4ce1fd
commit fad3257072
5 changed files with 270 additions and 18 deletions

View File

@ -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:

View File

@ -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) {

View File

@ -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();
}

View File

@ -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<ExoTrackSelection> @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<ExoTrackSelection>[][]) 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() {}
}
}

View File

@ -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<Throwable> exception = new AtomicReference<>();
AtomicReference<DownloadHelper> 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<Throwable> exception = new AtomicReference<>();
AtomicReference<DownloadHelper> 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<Throwable> exception = new AtomicReference<>();
AtomicReference<DownloadHelper> 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<Throwable> exception = new AtomicReference<>();
AtomicReference<DownloadHelper> 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<Throwable> exception = new AtomicReference<>();
AtomicReference<DownloadHelper> 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<Exception> prepareException = new AtomicReference<>(null);
CountDownLatch preparedLatch = new CountDownLatch(1);