From 9a616c0cee447b7bd809c0dfc4c9d864fc9fee56 Mon Sep 17 00:00:00 2001 From: tonihei Date: Wed, 13 Jul 2022 13:24:04 +0000 Subject: [PATCH] Use SingleThreadExecutor to release AudioTracks We currently start a simple Thread to release AudioTracks asynchronously. If many AudioTracks are released at the same time, this may lead to OOM situations because we attempt to create multiple new threads. This can be improved by using a shared SingleThreadExecutor. In the simple case of one simmultaneous release, it's exactly the same behavior as before: create a thread and release it as soon as it's done. For multiple simultanous releases we get the advantage of sharing a single thread to avoid creating more than one at the same time. Issue: google/ExoPlayer#10057 PiperOrigin-RevId: 460698942 --- RELEASENOTES.md | 3 + .../exoplayer/audio/DefaultAudioSink.java | 58 ++++++++++++++----- 2 files changed, 46 insertions(+), 15 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 425473041a..97aed03d91 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -14,6 +14,9 @@ ([#2518](https://github.com/google/ExoPlayer/issues/2518)). * Allow custom logger for all ExoPlayer log output ([#9752](https://github.com/google/ExoPlayer/issues/9752)). + * Use `SingleThreadExecutor` for releasing `AudioTrack` instances to avoid + OutOfMemory errors when releasing multiple players at the same time + ([#10057](https://github.com/google/ExoPlayer/issues/10057)). * Extractors: * Add support for AVI ([#2092](https://github.com/google/ExoPlayer/issues/2092)). diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/DefaultAudioSink.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/DefaultAudioSink.java index b53d79c47e..1704ed3ba3 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/DefaultAudioSink.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/DefaultAudioSink.java @@ -33,6 +33,7 @@ import android.os.SystemClock; import android.util.Pair; import androidx.annotation.DoNotInline; +import androidx.annotation.GuardedBy; import androidx.annotation.IntDef; import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; @@ -66,6 +67,7 @@ import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Collections; +import java.util.concurrent.ExecutorService; import org.checkerframework.checker.nullness.qual.MonotonicNonNull; import org.checkerframework.checker.nullness.qual.RequiresNonNull; @@ -472,6 +474,15 @@ public DefaultAudioSink build() { */ public static boolean failOnSpuriousAudioTimestamp = false; + private static final Object releaseExecutorLock = new Object(); + + @GuardedBy("releaseExecutorLock") + @Nullable + private static ExecutorService releaseExecutor; + + @GuardedBy("releaseExecutorLock") + private static int pendingReleaseCount; + private final AudioCapabilities audioCapabilities; private final AudioProcessorChain audioProcessorChain; private final boolean enableFloatOutput; @@ -1424,9 +1435,6 @@ public void flush() { if (isOffloadedPlayback(audioTrack)) { checkNotNull(offloadStreamEventCallbackV29).unregister(audioTrack); } - // AudioTrack.release can take some time, so we call it on a background thread. - final AudioTrack toRelease = audioTrack; - audioTrack = null; if (Util.SDK_INT < 21 && !externalAudioSessionIdProvided) { // Prior to API level 21, audio sessions are not kept alive once there are no components // associated with them. If we generated the session ID internally, the only component @@ -1440,18 +1448,8 @@ public void flush() { pendingConfiguration = null; } audioTrackPositionTracker.reset(); - releasingConditionVariable.close(); - new Thread("ExoPlayer:AudioTrackReleaseThread") { - @Override - public void run() { - try { - toRelease.flush(); - toRelease.release(); - } finally { - releasingConditionVariable.open(); - } - } - }.start(); + releaseAudioTrackAsync(audioTrack, releasingConditionVariable); + audioTrack = null; } writeExceptionPendingExceptionHolder.clear(); initializationExceptionPendingExceptionHolder.clear(); @@ -1862,6 +1860,36 @@ private void playPendingData() { } } + private static void releaseAudioTrackAsync( + AudioTrack audioTrack, ConditionVariable releasedConditionVariable) { + // AudioTrack.release can take some time, so we call it on a background thread. The background + // thread is shared statically to avoid creating many threads when multiple players are released + // at the same time. + releasedConditionVariable.close(); + synchronized (releaseExecutorLock) { + if (releaseExecutor == null) { + releaseExecutor = Util.newSingleThreadExecutor("ExoPlayer:AudioTrackReleaseThread"); + } + pendingReleaseCount++; + releaseExecutor.execute( + () -> { + try { + audioTrack.flush(); + audioTrack.release(); + } finally { + releasedConditionVariable.open(); + synchronized (releaseExecutorLock) { + pendingReleaseCount--; + if (pendingReleaseCount == 0) { + releaseExecutor.shutdown(); + releaseExecutor = null; + } + } + } + }); + } + } + @RequiresApi(29) private final class StreamEventCallbackV29 { private final Handler handler;