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;