From 73f97c0371939dc9e32f3091a811a903ae7709af Mon Sep 17 00:00:00 2001 From: tonihei Date: Fri, 11 Oct 2024 05:21:07 -0700 Subject: [PATCH] Allow AudioTrack to be provided by a customizable provider PiperOrigin-RevId: 684800579 --- .../exoplayer/audio/DefaultAudioSink.java | 158 +++++++++--------- .../audio/DefaultAudioTrackProvider.java | 118 +++++++++++++ 2 files changed, 193 insertions(+), 83 deletions(-) create mode 100644 libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/DefaultAudioTrackProvider.java 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 b4a21d94c1..aadbe5d945 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 @@ -92,6 +92,17 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; @UnstableApi public final class DefaultAudioSink implements AudioSink { + /** Provider for {@link AudioTrack} instances. */ + public interface AudioTrackProvider { + + /** The default provider for {@link AudioTrack} instances. */ + AudioTrackProvider DEFAULT = new DefaultAudioTrackProvider(); + + /** Returns a new {@link AudioTrack} for the given parameters. */ + AudioTrack getAudioTrack( + AudioTrackConfig audioTrackConfig, AudioAttributes audioAttributes, int audioSessionId); + } + /** * If an attempt to instantiate an AudioTrack with a buffer size larger than this value fails, a * second attempt is made using this buffer size. @@ -271,6 +282,7 @@ public final class DefaultAudioSink implements AudioSink { private boolean buildCalled; private AudioTrackBufferSizeProvider audioTrackBufferSizeProvider; + private AudioTrackProvider audioTrackProvider; private @MonotonicNonNull AudioOffloadSupportProvider audioOffloadSupportProvider; @Nullable private AudioOffloadListener audioOffloadListener; @@ -282,6 +294,7 @@ public final class DefaultAudioSink implements AudioSink { this.context = null; audioCapabilities = DEFAULT_AUDIO_CAPABILITIES; audioTrackBufferSizeProvider = AudioTrackBufferSizeProvider.DEFAULT; + audioTrackProvider = AudioTrackProvider.DEFAULT; } /** @@ -293,6 +306,7 @@ public final class DefaultAudioSink implements AudioSink { this.context = context; audioCapabilities = DEFAULT_AUDIO_CAPABILITIES; audioTrackBufferSizeProvider = AudioTrackBufferSizeProvider.DEFAULT; + audioTrackProvider = AudioTrackProvider.DEFAULT; } /** @@ -406,6 +420,18 @@ public final class DefaultAudioSink implements AudioSink { return this; } + /** + * Sets the {@link AudioTrackProvider} used to create {@link AudioTrack} instances. + * + * @param audioTrackProvider The {@link AudioTrackProvider}. + * @return This builder. + */ + @CanIgnoreReturnValue + public Builder setAudioTrackProvider(AudioTrackProvider audioTrackProvider) { + this.audioTrackProvider = audioTrackProvider; + return this; + } + /** Builds the {@link DefaultAudioSink}. Must only be called once per Builder instance. */ public DefaultAudioSink build() { checkState(!buildCalled); @@ -505,6 +531,7 @@ public final class DefaultAudioSink implements AudioSink { private final AudioTrackBufferSizeProvider audioTrackBufferSizeProvider; private final AudioOffloadSupportProvider audioOffloadSupportProvider; @Nullable private final AudioOffloadListener audioOffloadListener; + private final AudioTrackProvider audioTrackProvider; @Nullable private PlayerId playerId; @Nullable private Listener listener; @@ -590,6 +617,7 @@ public final class DefaultAudioSink implements AudioSink { initializationExceptionPendingExceptionHolder = new PendingExceptionHolder<>(); writeExceptionPendingExceptionHolder = new PendingExceptionHolder<>(); audioOffloadListener = builder.audioOffloadListener; + audioTrackProvider = builder.audioTrackProvider; } // AudioSink implementation. @@ -1045,7 +1073,12 @@ public final class DefaultAudioSink implements AudioSink { private AudioTrack buildAudioTrack(Configuration configuration) throws InitializationException { try { - AudioTrack audioTrack = configuration.buildAudioTrack(audioAttributes, audioSessionId); + AudioTrack audioTrack = + buildAudioTrack( + configuration.buildAudioTrackConfig(), + audioAttributes, + audioSessionId, + configuration.inputFormat); if (audioOffloadListener != null) { audioOffloadListener.onOffloadedPlayback(isOffloadedPlayback(audioTrack)); } @@ -1058,6 +1091,47 @@ public final class DefaultAudioSink implements AudioSink { } } + private AudioTrack buildAudioTrack( + AudioTrackConfig audioTrackConfig, + AudioAttributes audioAttributes, + int audioSessionId, + Format inputFormat) + throws InitializationException { + AudioTrack audioTrack; + try { + audioTrack = + audioTrackProvider.getAudioTrack(audioTrackConfig, audioAttributes, audioSessionId); + } catch (UnsupportedOperationException | IllegalArgumentException e) { + throw new InitializationException( + AudioTrack.STATE_UNINITIALIZED, + audioTrackConfig.sampleRate, + audioTrackConfig.channelConfig, + audioTrackConfig.encoding, + inputFormat, + /* isRecoverable= */ audioTrackConfig.offload, + e); + } + + int state = audioTrack.getState(); + if (state != AudioTrack.STATE_INITIALIZED) { + try { + audioTrack.release(); + } catch (Exception e) { + // The track has already failed to initialize, so it wouldn't be that surprising if + // release were to fail too. Swallow the exception. + } + throw new InitializationException( + state, + audioTrackConfig.sampleRate, + audioTrackConfig.channelConfig, + audioTrackConfig.encoding, + inputFormat, + /* isRecoverable= */ audioTrackConfig.offload, + /* audioTrackException= */ null); + } + return audioTrack; + } + @RequiresApi(29) private void registerStreamEventCallbackV29(AudioTrack audioTrack) { if (offloadStreamEventCallbackV29 == null) { @@ -2199,88 +2273,6 @@ public final class DefaultAudioSink implements AudioSink { bufferSize); } - public AudioTrack buildAudioTrack(AudioAttributes audioAttributes, int audioSessionId) - throws InitializationException { - AudioTrack audioTrack; - try { - audioTrack = createAudioTrack(audioAttributes, audioSessionId); - } catch (UnsupportedOperationException | IllegalArgumentException e) { - throw new InitializationException( - AudioTrack.STATE_UNINITIALIZED, - outputSampleRate, - outputChannelConfig, - bufferSize, - inputFormat, - /* isRecoverable= */ outputModeIsOffload(), - e); - } - - int state = audioTrack.getState(); - if (state != AudioTrack.STATE_INITIALIZED) { - try { - audioTrack.release(); - } catch (Exception e) { - // The track has already failed to initialize, so it wouldn't be that surprising if - // release were to fail too. Swallow the exception. - } - throw new InitializationException( - state, - outputSampleRate, - outputChannelConfig, - bufferSize, - inputFormat, - /* isRecoverable= */ outputModeIsOffload(), - /* audioTrackException= */ null); - } - return audioTrack; - } - - private AudioTrack createAudioTrack(AudioAttributes audioAttributes, int audioSessionId) { - if (Util.SDK_INT >= 29) { - return createAudioTrackV29(audioAttributes, audioSessionId); - } else { - return createAudioTrackV21(audioAttributes, audioSessionId); - } - } - - @RequiresApi(29) - private AudioTrack createAudioTrackV29(AudioAttributes audioAttributes, int audioSessionId) { - AudioFormat audioFormat = - Util.getAudioFormat(outputSampleRate, outputChannelConfig, outputEncoding); - android.media.AudioAttributes audioTrackAttributes = - getAudioTrackAttributes(audioAttributes, tunneling); - return new AudioTrack.Builder() - .setAudioAttributes(audioTrackAttributes) - .setAudioFormat(audioFormat) - .setTransferMode(AudioTrack.MODE_STREAM) - .setBufferSizeInBytes(bufferSize) - .setSessionId(audioSessionId) - .setOffloadedPlayback(outputMode == OUTPUT_MODE_OFFLOAD) - .build(); - } - - private AudioTrack createAudioTrackV21(AudioAttributes audioAttributes, int audioSessionId) { - return new AudioTrack( - getAudioTrackAttributes(audioAttributes, tunneling), - Util.getAudioFormat(outputSampleRate, outputChannelConfig, outputEncoding), - bufferSize, - AudioTrack.MODE_STREAM, - audioSessionId); - } - - private static android.media.AudioAttributes getAudioTrackAttributes( - AudioAttributes audioAttributes, boolean tunneling) { - if (tunneling) { - return new android.media.AudioAttributes.Builder() - .setContentType(android.media.AudioAttributes.CONTENT_TYPE_MOVIE) - .setFlags(android.media.AudioAttributes.FLAG_HW_AV_SYNC) - .setUsage(android.media.AudioAttributes.USAGE_MEDIA) - .build(); - } else { - return audioAttributes.getAudioAttributesV21().audioAttributes; - } - } - public boolean outputModeIsOffload() { return outputMode == OUTPUT_MODE_OFFLOAD; } diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/DefaultAudioTrackProvider.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/DefaultAudioTrackProvider.java new file mode 100644 index 0000000000..d59fca48aa --- /dev/null +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/DefaultAudioTrackProvider.java @@ -0,0 +1,118 @@ +/* + * 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 + * + * https://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 android.media.AudioFormat; +import android.media.AudioTrack; +import androidx.annotation.RequiresApi; +import androidx.media3.common.AudioAttributes; +import androidx.media3.common.util.UnstableApi; +import androidx.media3.common.util.Util; +import com.google.errorprone.annotations.CanIgnoreReturnValue; + +/** + * The default provider for {@link AudioTrack} instances. + * + *

Subclasses of this provider can customize the {@link AudioTrack.Builder} in {@link + * #customizeAudioTrackBuilder} if required. + */ +@UnstableApi +public class DefaultAudioTrackProvider implements DefaultAudioSink.AudioTrackProvider { + + @Override + public final AudioTrack getAudioTrack( + AudioSink.AudioTrackConfig audioTrackConfig, + AudioAttributes audioAttributes, + int audioSessionId) { + if (Util.SDK_INT >= 23) { + return createAudioTrackV23(audioTrackConfig, audioAttributes, audioSessionId); + } else { + return createAudioTrackV21(audioTrackConfig, audioAttributes, audioSessionId); + } + } + + @RequiresApi(23) + private AudioTrack createAudioTrackV23( + AudioSink.AudioTrackConfig audioTrackConfig, + AudioAttributes audioAttributes, + int audioSessionId) { + AudioFormat audioFormat = + Util.getAudioFormat( + audioTrackConfig.sampleRate, audioTrackConfig.channelConfig, audioTrackConfig.encoding); + android.media.AudioAttributes audioTrackAttributes = + getAudioTrackAttributesV21(audioAttributes, audioTrackConfig.tunneling); + AudioTrack.Builder audioTrackBuilder = + new AudioTrack.Builder() + .setAudioAttributes(audioTrackAttributes) + .setAudioFormat(audioFormat) + .setTransferMode(AudioTrack.MODE_STREAM) + .setBufferSizeInBytes(audioTrackConfig.bufferSize) + .setSessionId(audioSessionId); + if (Util.SDK_INT >= 29) { + setOffloadedPlaybackV29(audioTrackBuilder, audioTrackConfig.offload); + } + return customizeAudioTrackBuilder(audioTrackBuilder).build(); + } + + @RequiresApi(29) + private void setOffloadedPlaybackV29(AudioTrack.Builder audioTrackBuilder, boolean isOffloaded) { + audioTrackBuilder.setOffloadedPlayback(isOffloaded); + } + + /** + * Optionally customize {@link AudioTrack.Builder} with other parameters. + * + *

Note that this method is only called on API 23 and above. + * + * @param audioTrackBuilder The {@link AudioTrack.Builder} on which to set the attributes. + * @return The same {@link AudioTrack.Builder} instance provided. + */ + @RequiresApi(23) // AudioTrack.Builder is available starting from API 23. + @CanIgnoreReturnValue + protected AudioTrack.Builder customizeAudioTrackBuilder(AudioTrack.Builder audioTrackBuilder) { + return audioTrackBuilder; + } + + private AudioTrack createAudioTrackV21( + AudioSink.AudioTrackConfig audioTrackConfig, + AudioAttributes audioAttributes, + int audioSessionId) { + return new AudioTrack( + getAudioTrackAttributesV21(audioAttributes, audioTrackConfig.tunneling), + Util.getAudioFormat( + audioTrackConfig.sampleRate, audioTrackConfig.channelConfig, audioTrackConfig.encoding), + audioTrackConfig.bufferSize, + AudioTrack.MODE_STREAM, + audioSessionId); + } + + private android.media.AudioAttributes getAudioTrackAttributesV21( + AudioAttributes audioAttributes, boolean tunneling) { + if (tunneling) { + return getAudioTrackTunnelingAttributesV21(); + } else { + return audioAttributes.getAudioAttributesV21().audioAttributes; + } + } + + private android.media.AudioAttributes getAudioTrackTunnelingAttributesV21() { + return new android.media.AudioAttributes.Builder() + .setContentType(android.media.AudioAttributes.CONTENT_TYPE_MOVIE) + .setFlags(android.media.AudioAttributes.FLAG_HW_AV_SYNC) + .setUsage(android.media.AudioAttributes.USAGE_MEDIA) + .build(); + } +}