Create new AudioTrack without waiting for previous released tracks

We currently wait until a previous AudioTrack from the same
DefaultAudioSink is released on a background thread before attempting
to initialize a new AudioTrack. This is done to avoid issues where
the releasing track blocks some shared audio memory, preventing a new
track from being created.

The current solution has two main shortcomings:
 - In most cases, the system can easily handle multiple AudioTracks
   and waiting for the release just causes unnecessary delays (e.g.
   when seeking).
 - It only waits for a previous track from the same DefaultAudioSink,
   not accounting for any other tracks that may be in the process of
   being released from other players.

To improve on both shortcomings, we can
 (1) move the check for "is releasing tracks and thus may block shared
 memory" to the static release infrastructure to be shared across all
 player instances.
 (2) optimistically create a new AudioTrack immediately without waiting
 for the previous one to be fully released.
 (3) extend the existing retry logic that already retries failed
 attempts for 100ms to only start the timer when ongoing releases are
 done. This ensures we really waited until we have all shared resources
 we can get before giving up completely. This also acts as a replacement
 for change (2) to handle situations where creating a second track is
 genuinely not possible. Also increase threshold to 200ms as the new
 unit test is falky on a real device with just 100ms (highlighting that
 the device needed more than 100ms to clean up internal resources).

PiperOrigin-RevId: 654053123
This commit is contained in:
tonihei 2024-07-19 10:38:39 -07:00 committed by Copybara-Service
parent 076bc451f2
commit 0a8ca18305
2 changed files with 143 additions and 32 deletions

View File

@ -0,0 +1,92 @@
/*
* Copyright 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.media3.exoplayer.audio;
import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
import static com.google.common.collect.Iterables.getLast;
import android.content.Context;
import androidx.media3.common.C;
import androidx.media3.common.Format;
import androidx.media3.common.MimeTypes;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.ArrayList;
import org.junit.Test;
import org.junit.runner.RunWith;
/** Instrumentation unit tests for {@link DefaultAudioSink}. */
@RunWith(AndroidJUnit4.class)
public class DefaultAudioSinkTest {
@Test
public void audioTrackExceedsSharedMemory_retriesUntilOngoingReleasesAreDone() throws Exception {
Context context = ApplicationProvider.getApplicationContext();
getInstrumentation()
.runOnMainSync(
() -> {
// Create audio sinks in parallel until we exceed the device's shared audio memory.
ArrayList<DefaultAudioSink> audioSinks = new ArrayList<>();
while (true) {
DefaultAudioSink audioSink = new DefaultAudioSink.Builder(context).build();
audioSinks.add(audioSink);
try {
configureAudioSinkAndFeedData(audioSink);
} catch (Exception e) {
// Expected to happen once we reached the shared audio memory limit of the device.
break;
}
}
// Trigger release of one sink and immediately try the failed sink again. This should
// now succeed even if the sink is released asynchronously.
audioSinks.get(0).flush();
audioSinks.get(0).release();
try {
configureAudioSinkAndFeedData(getLast(audioSinks));
} catch (Exception e) {
throw new IllegalStateException(e);
}
// Clean-up
for (int i = 1; i < audioSinks.size(); i++) {
audioSinks.get(i).flush();
audioSinks.get(i).release();
}
});
}
private void configureAudioSinkAndFeedData(DefaultAudioSink audioSink) throws Exception {
Format format =
new Format.Builder()
.setSampleMimeType(MimeTypes.AUDIO_RAW)
.setPcmEncoding(C.ENCODING_PCM_16BIT)
.setChannelCount(2)
.setSampleRate(44_100)
.build();
audioSink.configure(format, /* specifiedBufferSize= */ 2_000_000, /* outputChannels= */ null);
audioSink.play();
ByteBuffer buffer = ByteBuffer.allocateDirect(8000).order(ByteOrder.nativeOrder());
boolean handledBuffer = false;
while (!handledBuffer) {
handledBuffer =
audioSink.handleBuffer(
buffer, /* presentationTimeUs= */ 0, /* encodedAccessUnitCount= */ 1);
}
}
}

View File

@ -55,7 +55,6 @@ import androidx.media3.common.audio.SonicAudioProcessor;
import androidx.media3.common.audio.ToInt16PcmAudioProcessor;
import androidx.media3.common.util.Assertions;
import androidx.media3.common.util.Clock;
import androidx.media3.common.util.ConditionVariable;
import androidx.media3.common.util.Log;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.common.util.Util;
@ -463,12 +462,6 @@ public final class DefaultAudioSink implements AudioSink {
*/
private static final int ERROR_NATIVE_DEAD_OBJECT = -32;
/**
* The duration for which failed attempts to initialize or write to the audio track may be retried
* before throwing an exception, in milliseconds.
*/
private static final int AUDIO_TRACK_RETRY_DURATION_MS = 100;
private static final String TAG = "DefaultAudioSink";
/**
@ -496,7 +489,6 @@ public final class DefaultAudioSink implements AudioSink {
private final TrimmingAudioProcessor trimmingAudioProcessor;
private final ImmutableList<AudioProcessor> toIntPcmAvailableAudioProcessors;
private final ImmutableList<AudioProcessor> toFloatPcmAvailableAudioProcessors;
private final ConditionVariable releasingConditionVariable;
private final AudioTrackPositionTracker audioTrackPositionTracker;
private final ArrayDeque<MediaPositionParameters> mediaPositionParametersCheckpoints;
private final boolean preferAudioTrackPlaybackParams;
@ -574,8 +566,6 @@ public final class DefaultAudioSink implements AudioSink {
offloadMode = OFFLOAD_MODE_DISABLED;
audioTrackBufferSizeProvider = builder.audioTrackBufferSizeProvider;
audioOffloadSupportProvider = checkNotNull(builder.audioOffloadSupportProvider);
releasingConditionVariable = new ConditionVariable(Clock.DEFAULT);
releasingConditionVariable.open();
audioTrackPositionTracker = new AudioTrackPositionTracker(new PositionTrackerListener());
channelMappingAudioProcessor = new ChannelMappingAudioProcessor();
trimmingAudioProcessor = new TrimmingAudioProcessor();
@ -592,10 +582,8 @@ public final class DefaultAudioSink implements AudioSink {
playbackParameters = PlaybackParameters.DEFAULT;
skipSilenceEnabled = DEFAULT_SKIP_SILENCE;
mediaPositionParametersCheckpoints = new ArrayDeque<>();
initializationExceptionPendingExceptionHolder =
new PendingExceptionHolder<>(AUDIO_TRACK_RETRY_DURATION_MS);
writeExceptionPendingExceptionHolder =
new PendingExceptionHolder<>(AUDIO_TRACK_RETRY_DURATION_MS);
initializationExceptionPendingExceptionHolder = new PendingExceptionHolder<>();
writeExceptionPendingExceptionHolder = new PendingExceptionHolder<>();
audioOffloadListener = builder.audioOffloadListener;
}
@ -806,12 +794,7 @@ public final class DefaultAudioSink implements AudioSink {
}
private boolean initializeAudioTrack() throws InitializationException {
// If we're asynchronously releasing a previous audio track then we wait until it has been
// released. This guarantees that we cannot end up in a state where we have multiple audio
// track instances. Without this guarantee it would be possible, in extreme cases, to exhaust
// the shared memory that's available for audio track buffers. This would in turn cause the
// initialization of the audio track to fail.
if (!releasingConditionVariable.isOpen()) {
if (initializationExceptionPendingExceptionHolder.shouldWaitBeforeRetry()) {
return false;
}
@ -1151,6 +1134,9 @@ public final class DefaultAudioSink implements AudioSink {
} else {
outputBuffer = buffer;
}
if (writeExceptionPendingExceptionHolder.shouldWaitBeforeRetry()) {
return;
}
int bytesRemaining = buffer.remaining();
int bytesWrittenOrError = 0; // Error if negative
if (tunneling) {
@ -1446,7 +1432,7 @@ public final class DefaultAudioSink implements AudioSink {
onRoutingChangedListener.release();
onRoutingChangedListener = null;
}
releaseAudioTrackAsync(audioTrack, releasingConditionVariable, listener, oldAudioTrackConfig);
releaseAudioTrackAsync(audioTrack, listener, oldAudioTrackConfig);
audioTrack = null;
}
writeExceptionPendingExceptionHolder.clear();
@ -1834,14 +1820,10 @@ public final class DefaultAudioSink implements AudioSink {
}
private static void releaseAudioTrackAsync(
AudioTrack audioTrack,
ConditionVariable releasedConditionVariable,
@Nullable Listener listener,
AudioTrackConfig audioTrackConfig) {
AudioTrack audioTrack, @Nullable Listener listener, AudioTrackConfig audioTrackConfig) {
// 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();
Handler audioTrackThreadHandler = new Handler(Looper.myLooper());
synchronized (releaseExecutorLock) {
if (releaseExecutor == null) {
@ -1857,7 +1839,6 @@ public final class DefaultAudioSink implements AudioSink {
if (listener != null && audioTrackThreadHandler.getLooper().getThread().isAlive()) {
audioTrackThreadHandler.post(() -> listener.onAudioTrackReleased(audioTrackConfig));
}
releasedConditionVariable.open();
synchronized (releaseExecutorLock) {
pendingReleaseCount--;
if (pendingReleaseCount == 0) {
@ -1870,6 +1851,12 @@ public final class DefaultAudioSink implements AudioSink {
}
}
private static boolean hasPendingAudioTrackReleases() {
synchronized (releaseExecutorLock) {
return pendingReleaseCount > 0;
}
}
@RequiresApi(24)
private static final class OnRoutingChangedListenerApi24 {
@ -2246,22 +2233,39 @@ public final class DefaultAudioSink implements AudioSink {
private static final class PendingExceptionHolder<T extends Exception> {
private final long throwDelayMs;
/**
* The duration for which failed audio track operations may be retried before throwing an
* exception, in milliseconds. This duration is needed because audio tracks may retain some
* resources for a short time even after they are released. Waiting a bit longer allows the
* AudioFlinger to close all HAL streams that still hold resources. See b/167682058 and
* https://github.com/google/ExoPlayer/issues/4448.
*/
private static final int RETRY_DURATION_MS = 200;
/** Minimum delay between two retries. */
private static final int RETRY_DELAY_MS = 50;
@Nullable private T pendingException;
private long throwDeadlineMs;
private long earliestNextRetryTimeMs;
public PendingExceptionHolder(long throwDelayMs) {
this.throwDelayMs = throwDelayMs;
public PendingExceptionHolder() {
this.throwDeadlineMs = C.TIME_UNSET;
this.earliestNextRetryTimeMs = C.TIME_UNSET;
}
public void throwExceptionIfDeadlineIsReached(T exception) throws T {
long nowMs = SystemClock.elapsedRealtime();
if (pendingException == null) {
pendingException = exception;
throwDeadlineMs = nowMs + throwDelayMs;
}
if (nowMs >= throwDeadlineMs) {
if (throwDeadlineMs == C.TIME_UNSET && !hasPendingAudioTrackReleases()) {
// The audio system has limited shared memory. If there is an ongoing release, the audio
// track operation could be failing because this shared memory is exhausted (see
// b/12565083). Only start the retry timer once all pending audio track releases are done.
throwDeadlineMs = nowMs + RETRY_DURATION_MS;
}
if (throwDeadlineMs != C.TIME_UNSET && nowMs >= throwDeadlineMs) {
if (pendingException != exception) {
// All retry exception are probably the same, thus only save the last one to save memory.
pendingException.addSuppressed(exception);
@ -2270,10 +2274,25 @@ public final class DefaultAudioSink implements AudioSink {
clear();
throw pendingException;
}
earliestNextRetryTimeMs = nowMs + RETRY_DELAY_MS;
}
public boolean shouldWaitBeforeRetry() {
if (pendingException == null) {
// No pending exception.
return false;
}
if (hasPendingAudioTrackReleases()) {
// Wait until other tracks are released before retrying.
return true;
}
return SystemClock.elapsedRealtime() < earliestNextRetryTimeMs;
}
public void clear() {
pendingException = null;
throwDeadlineMs = C.TIME_UNSET;
earliestNextRetryTimeMs = C.TIME_UNSET;
}
}