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 c702bc6dd6d..b7fc65ecd81 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 @@ -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; @@ -58,6 +59,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; @@ -463,6 +465,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; @@ -1415,9 +1426,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 @@ -1431,18 +1439,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(); @@ -1853,6 +1851,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;