Publish some internal tests now we depend on Robolectric 4.14.1

PiperOrigin-RevId: 705411400
This commit is contained in:
ibaker 2024-12-12 01:25:50 -08:00 committed by Copybara-Service
parent f587ac2a67
commit 0b0c198f59
4 changed files with 1729 additions and 0 deletions

View File

@ -0,0 +1,583 @@
/*
* 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 android.media.AudioFormat.CHANNEL_OUT_5POINT1;
import static android.media.AudioFormat.CHANNEL_OUT_STEREO;
import static android.media.AudioFormat.ENCODING_AC3;
import static androidx.media3.common.util.Assertions.checkNotNull;
import static androidx.media3.test.utils.robolectric.TestPlayerRunHelper.run;
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.robolectric.Shadows.shadowOf;
import android.app.UiModeManager;
import android.content.Context;
import android.content.res.Configuration;
import android.media.AudioAttributes;
import android.media.AudioDeviceInfo;
import android.media.AudioFormat;
import android.media.AudioManager;
import android.media.AudioProfile;
import android.media.MediaCodec;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.media3.common.MediaItem;
import androidx.media3.common.MimeTypes;
import androidx.media3.common.Player;
import androidx.media3.common.Tracks;
import androidx.media3.common.Tracks.Group;
import androidx.media3.common.util.Util;
import androidx.media3.exoplayer.DecoderCounters;
import androidx.media3.exoplayer.ExoPlayer;
import androidx.media3.exoplayer.Renderer;
import androidx.media3.exoplayer.RenderersFactory;
import androidx.media3.exoplayer.analytics.AnalyticsListener;
import androidx.media3.exoplayer.mediacodec.MediaCodecSelector;
import androidx.media3.exoplayer.mediacodec.SynchronousMediaCodecAdapter;
import androidx.media3.exoplayer.trackselection.DefaultTrackSelector.Parameters;
import androidx.media3.test.utils.FakeClock;
import androidx.media3.test.utils.robolectric.ShadowMediaCodecConfig;
import androidx.media3.test.utils.robolectric.TestPlayerRunHelper;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.robolectric.annotation.Config;
import org.robolectric.shadows.AudioDeviceInfoBuilder;
import org.robolectric.shadows.AudioProfileBuilder;
import org.robolectric.shadows.ShadowAudioTrack;
import org.robolectric.shadows.ShadowUIModeManager;
/** End to end playback test for audio capabilities. */
@RunWith(AndroidJUnit4.class)
public class AudioCapabilitiesEndToEndTest {
private Context applicationContext;
private AudioManager audioManager;
private Parameters defaultParameters;
@Nullable private AudioDeviceInfo directPlaybackDevice;
private List<Tracks> selectedTracks;
private List<String> analyticsListenerReceivedCallbacks;
@Rule
public ShadowMediaCodecConfig shadowMediaCodecConfig =
ShadowMediaCodecConfig.withNoDefaultSupportedMimeTypes();
@Before
public void setUp() {
applicationContext = ApplicationProvider.getApplicationContext();
audioManager = (AudioManager) applicationContext.getSystemService(Context.AUDIO_SERVICE);
shadowOf(audioManager).setOutputDevices(ImmutableList.of());
defaultParameters = Parameters.getDefaults(applicationContext);
selectedTracks = new ArrayList<>();
analyticsListenerReceivedCallbacks = new ArrayList<>();
}
@After
public void tearDown() {
clearDirectPlaybackSupport();
}
/**
* Tests that ExoPlayer recovers from AudioTrack recoverable error.
*
* <p>The test starts playing of AC3 audio via audio passthrough (direct playback). Mid-playback,
* {@link android.media.AudioTrack} no longer supports direct playback of the format, emulating a
* TV where the routed audio device moves from TV speakers to a bluetooth headset, and writing to
* the {@link android.media.AudioTrack} returns {@link
* android.media.AudioTrack#ERROR_DEAD_OBJECT}. As a result, the player triggers a new track
* selection and picks the AAC stereo format which is available.
*
* <p>For {@code API 33+}, the {@link AudioCapabilities} polls the platform with {@link
* AudioManager#getDirectProfilesForAttributes(AudioAttributes)}. And for {@code 29 <= API < 33},
* the {@link AudioCapabilities} polls the platform with {@link
* android.media.AudioTrack#isDirectPlaybackSupported}. Use the {@link
* SynchronousMediaCodecAdapter} because {@link MediaCodec} is not fully functional in
* asynchronous mode with Robolectric.
*/
@Test
@Config(minSdk = 29)
public void playAc3WithDirectPlayback_directPlaybackNotSupportMidPlayback_recoversToAac()
throws Exception {
shadowMediaCodecConfig.addSupportedMimeTypes(MimeTypes.AUDIO_AAC);
setupDefaultPcmSupport();
addDirectPlaybackSupportForAC3();
setUiModeToTv();
RenderersFactory renderersFactory =
createRenderersFactory(
// Right after we write the first buffer to audio sink, AudioTrack stops
// supporting the audio format directly.
/* onProcessedOutputBufferRunnable= */ this::clearDirectPlaybackSupport);
ExoPlayer player =
new ExoPlayer.Builder(applicationContext, renderersFactory)
.setClock(new FakeClock(/* isAutoAdvancing= */ true))
.build();
player.setMediaItem(MediaItem.fromUri("asset:///media/mp4/sample_ac3_aac.mp4"));
player.prepare();
Player.Listener listener = mock(Player.Listener.class);
player.addListener(listener);
player.play();
run(player).ignoringNonFatalErrors().untilState(Player.STATE_ENDED);
player.release();
ArgumentCaptor<Tracks> tracks = ArgumentCaptor.forClass(Tracks.class);
verify(listener, times(2)).onTracksChanged(tracks.capture());
// First track selection picks AC3 and second track selection picks AAC.
Tracks firstTrackSelection = tracks.getAllValues().get(0);
assertThat(firstTrackSelection.getGroups()).hasSize(2);
assertThat(firstTrackSelection.getGroups().get(0).isSelected()).isTrue();
assertThat(firstTrackSelection.getGroups().get(0).getTrackFormat(0).sampleMimeType)
.isEqualTo(MimeTypes.AUDIO_AC3);
assertThat(firstTrackSelection.getGroups().get(1).isSelected()).isFalse();
assertThat(firstTrackSelection.getGroups().get(1).getTrackFormat(0).sampleMimeType)
.isEqualTo(MimeTypes.AUDIO_AAC);
Tracks secondTrackSelection = tracks.getAllValues().get(1);
assertThat(secondTrackSelection.getGroups()).hasSize(2);
assertThat(secondTrackSelection.getGroups().get(0).isSelected()).isFalse();
assertThat(secondTrackSelection.getGroups().get(0).getTrackFormat(0).sampleMimeType)
.isEqualTo(MimeTypes.AUDIO_AC3);
assertThat(secondTrackSelection.getGroups().get(1).isSelected()).isTrue();
assertThat(secondTrackSelection.getGroups().get(1).getTrackFormat(0).sampleMimeType)
.isEqualTo(MimeTypes.AUDIO_AAC);
}
/**
* Tests that ExoPlayer recovers from AudioTrack recoverable error.
*
* <p>The test starts playing of AC3 audio via audio passthrough (direct playback). Mid-playback,
* {@link android.media.AudioTrack} no longer supports direct playback of the format, emulating a
* TV where the routed audio device moves from TV speakers to a bluetooth headset, and writing to
* the {@link android.media.AudioTrack} returns {@link
* android.media.AudioTrack#ERROR_DEAD_OBJECT}. In this test, the device also has an AC3 decoder,
* so after the recovery, the AC3 audio is chosen again.
*
* <p>For {@code API 33+}, the {@link AudioCapabilities} polls the platform with {@link
* AudioManager#getDirectProfilesForAttributes(AudioAttributes)}. And for {@code 29 <= API < 33},
* the {@link AudioCapabilities} polls the platform with {@link
* android.media.AudioTrack#isDirectPlaybackSupported}. Use the {@link
* SynchronousMediaCodecAdapter} because {@link MediaCodec} is not fully functional in
* asynchronous mode with Robolectric.
*/
@Test
@Config(minSdk = 29)
public void
playAc3WithDirectPlayback_directPlaybackNotSupportMidPlaybackButDeviceHasAc3Codec_recoversToAc3()
throws Throwable {
shadowMediaCodecConfig.addSupportedMimeTypes(MimeTypes.AUDIO_AAC, MimeTypes.AUDIO_AC3);
setupDefaultPcmSupport();
addDirectPlaybackSupportForAC3();
setUiModeToTv();
RenderersFactory renderersFactory =
createRenderersFactory(
// Right after we write the first buffer to audio sink, AudioTrack stops
// supporting the audio format directly.
/* onProcessedOutputBufferRunnable= */ this::clearDirectPlaybackSupport);
ExoPlayer player =
new ExoPlayer.Builder(applicationContext, renderersFactory)
.setClock(new FakeClock(/* isAutoAdvancing= */ true))
.build();
player.setMediaItem(MediaItem.fromUri("asset:///media/mp4/sample_ac3_aac.mp4"));
player.prepare();
player.addAnalyticsListener(createAnalyticsListener());
player.play();
run(player).ignoringNonFatalErrors().untilState(Player.STATE_ENDED);
player.release();
// We expect to start playing audio via passthrough and mid-playback switch to a local decoder.
// Hence, the audio renderer is enabled and disabled without an audio decoder initialized,
// indicating direct audio playback. Then the audio renderer is enabled and an audio decoder is
// initialized, indicating local decoding of audio. We cannot verify the order of callbacks with
// Mockito's in-order verification because onAudioEnabled is called twice and we cannot reliably
// distinguish between the two calls with Mockito.
assertThat(analyticsListenerReceivedCallbacks)
.containsExactly(
"onAudioEnabled", "onAudioDisabled", "onAudioEnabled", "onAudioDecoderInitialized")
.inOrder();
// Verify onTracksChanged was called exactly once and the AC3 track was selected.
ImmutableList<Group> groups = Iterables.getOnlyElement(selectedTracks).getGroups();
assertThat(groups.get(0).isSelected()).isTrue();
assertThat(groups.get(0).getTrackFormat(0).sampleMimeType).isEqualTo(MimeTypes.AUDIO_AC3);
assertThat(groups.get(1).isSelected()).isFalse();
}
/**
* Tests that ExoPlayer switches to direct playback mode after audio capabilities change.
*
* <p>The test starts with playing AAC stereo audio over a bluetooth headset, and the {@linkplain
* Parameters.Builder#setAllowInvalidateSelectionsOnRendererCapabilitiesChange(boolean) is turned
* on}. Mid-playback, the bluetooth headset is disconnected and the default TV speaker supports
* direct playback for AC3. Then the AC3 audio is selected for direct playback.
*
* <p>For {@code API 33+}, the {@link AudioCapabilities} polls the platform with {@link
* AudioManager#getDirectProfilesForAttributes(AudioAttributes)}. And for {@code 29 <= API < 33},
* the {@link AudioCapabilities} polls the platform with {@link
* android.media.AudioTrack#isDirectPlaybackSupported}. Use the {@link
* SynchronousMediaCodecAdapter} because {@link MediaCodec} is not fully functional in
* asynchronous mode with Robolectric.
*/
@Test
@Config(minSdk = 29)
public void playAacWithCodec_directPlaybackSupportMidPlayback_changeToAc3DirectPlayback()
throws Throwable {
final AtomicBoolean directPlaybackSupportAddedReference = new AtomicBoolean();
setupDefaultPcmSupport();
shadowMediaCodecConfig.addSupportedMimeTypes(MimeTypes.AUDIO_AAC);
setUiModeToTv();
RenderersFactory renderersFactory =
createRenderersFactory(
/* onProcessedOutputBufferRunnable= */ () -> {
// Right after we write the first buffer to audio sink, direct playback support
// is added, and AudioTrack begins to support the audio format directly.
if (!directPlaybackSupportAddedReference.get()) {
addDirectPlaybackSupportForAC3();
directPlaybackSupportAddedReference.set(true);
}
});
ExoPlayer player =
new ExoPlayer.Builder(applicationContext, renderersFactory)
.setClock(new FakeClock(/* isAutoAdvancing= */ true))
.build();
player.setTrackSelectionParameters(
defaultParameters
.buildUpon()
.setAllowInvalidateSelectionsOnRendererCapabilitiesChange(true)
.build());
player.setMediaItem(MediaItem.fromUri("asset:///media/mp4/sample_ac3_aac.mp4"));
player.prepare();
Player.Listener listener = mock(Player.Listener.class);
player.addListener(listener);
player.play();
TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_ENDED);
player.release();
ArgumentCaptor<Tracks> tracks = ArgumentCaptor.forClass(Tracks.class);
verify(listener, times(2)).onTracksChanged(tracks.capture());
// First track selection picks AAC and second track selection picks AC3.
Tracks firstTrackSelection = tracks.getAllValues().get(0);
assertThat(firstTrackSelection.getGroups()).hasSize(2);
assertThat(firstTrackSelection.getGroups().get(0).isSelected()).isFalse();
assertThat(firstTrackSelection.getGroups().get(0).getTrackFormat(0).sampleMimeType)
.isEqualTo(MimeTypes.AUDIO_AC3);
assertThat(firstTrackSelection.getGroups().get(1).isSelected()).isTrue();
assertThat(firstTrackSelection.getGroups().get(1).getTrackFormat(0).sampleMimeType)
.isEqualTo(MimeTypes.AUDIO_AAC);
Tracks secondTrackSelection = tracks.getAllValues().get(1);
assertThat(secondTrackSelection.getGroups()).hasSize(2);
assertThat(secondTrackSelection.getGroups().get(0).isSelected()).isTrue();
assertThat(secondTrackSelection.getGroups().get(0).getTrackFormat(0).sampleMimeType)
.isEqualTo(MimeTypes.AUDIO_AC3);
assertThat(secondTrackSelection.getGroups().get(1).isSelected()).isFalse();
assertThat(secondTrackSelection.getGroups().get(1).getTrackFormat(0).sampleMimeType)
.isEqualTo(MimeTypes.AUDIO_AAC);
}
/**
* Tests that ExoPlayer switches to direct playback mode after audio capabilities change.
*
* <p>The test starts with playing over a bluetooth headset, the AC3 audio is decoded to PCM with
* a local decoder, and the {@linkplain
* Parameters.Builder#setAllowInvalidateSelectionsOnRendererCapabilitiesChange(boolean) is turned
* on}. Mid-playback, the bluetooth headset is disconnected and the default TV speaker supports
* direct playback for AC3. Then the AC3 audio is selected again for direct playback.
*
* <p>For {@code API 33+}, the {@link AudioCapabilities} polls the platform with {@link
* AudioManager#getDirectProfilesForAttributes(AudioAttributes)}. And for {@code 29 <= API < 33},
* the {@link AudioCapabilities} polls the platform with {@link
* android.media.AudioTrack#isDirectPlaybackSupported}. Use the {@link
* SynchronousMediaCodecAdapter} because {@link MediaCodec} is not fully functional in
* asynchronous mode with Robolectric.
*/
@Test
@Config(minSdk = 29)
public void playAc3WithCodec_directPlaybackSupportMidPlayback_changeToAc3DirectPlayback()
throws Throwable {
final AtomicBoolean directPlaybackSupportAddedReference = new AtomicBoolean();
setupDefaultPcmSupport();
shadowMediaCodecConfig.addSupportedMimeTypes(MimeTypes.AUDIO_AAC, MimeTypes.AUDIO_AC3);
setUiModeToTv();
RenderersFactory renderersFactory =
createRenderersFactory(
/* onProcessedOutputBufferRunnable= */ () -> {
// Right after we write the first buffer to audio sink, direct playback support
// is added, and AudioTrack begins to support the audio format directly.
if (!directPlaybackSupportAddedReference.get()) {
addDirectPlaybackSupportForAC3();
directPlaybackSupportAddedReference.set(true);
}
});
ExoPlayer player =
new ExoPlayer.Builder(applicationContext, renderersFactory)
.setClock(new FakeClock(/* isAutoAdvancing= */ true))
.build();
player.setTrackSelectionParameters(
defaultParameters
.buildUpon()
.setAllowInvalidateSelectionsOnRendererCapabilitiesChange(true)
.build());
player.setMediaItem(MediaItem.fromUri("asset:///media/mp4/sample_ac3_aac.mp4"));
player.prepare();
player.addAnalyticsListener(createAnalyticsListener());
player.play();
TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_ENDED);
player.release();
// We expect to start playing audio with a local AC3 decoder and mid-playback switch to the
// passthrough mode. Hence, the audio renderer is enabled and disabled with an audio decoder
// initialized, indicating the local decoding of audio. Then the audio renderer is enabled and
// the audio decoder is released, indicating the direct audio playback. We cannot verify the
// order of callbacks with Mockito's in-order verification because onAudioEnabled is called
// twice and we cannot reliably distinguish between the two calls with Mockito.
assertThat(analyticsListenerReceivedCallbacks)
.containsExactly(
"onAudioEnabled",
"onAudioDecoderInitialized",
"onAudioDisabled",
"onAudioEnabled",
"onAudioDecoderReleased")
.inOrder();
// Verify onTracksChanged was called exactly once and the AC3 track was selected.
ImmutableList<Group> groups = Iterables.getOnlyElement(selectedTracks).getGroups();
assertThat(groups.get(0).isSelected()).isTrue();
assertThat(groups.get(0).getTrackFormat(0).sampleMimeType).isEqualTo(MimeTypes.AUDIO_AC3);
assertThat(groups.get(1).isSelected()).isFalse();
}
/**
* Tests that ExoPlayer doesn't switch track after audio capabilities change when the {@linkplain
* Parameters.Builder#setAllowInvalidateSelectionsOnRendererCapabilitiesChange(boolean) is turned
* off}.
*
* <p>The test starts with playing AAC stereo audio over a bluetooth headset, and the {@linkplain
* Parameters.Builder#setAllowInvalidateSelectionsOnRendererCapabilitiesChange(boolean) is turned
* off}. Mid-playback, the bluetooth headset is disconnected and the default TV speaker supports
* direct playback for AC3. The playback remains with AAC track.
*
* <p>For {@code API 33+}, the {@link AudioCapabilities} polls the platform with {@link
* AudioManager#getDirectProfilesForAttributes(AudioAttributes)}. And for {@code 29 <= API < 33},
* the {@link AudioCapabilities} polls the platform with {@link
* android.media.AudioTrack#isDirectPlaybackSupported}. Use the {@link
* SynchronousMediaCodecAdapter} because {@link MediaCodec} is not fully functional in
* asynchronous mode with Robolectric.
*/
@Test
@Config(minSdk = 29)
public void playAacWithCodec_rendererCapabilitiesChangedWhenSelectionInvalidationTurnedOff()
throws Throwable {
final AtomicBoolean directPlaybackSupportAddedReference = new AtomicBoolean();
shadowMediaCodecConfig.addSupportedMimeTypes(MimeTypes.AUDIO_AAC);
setupDefaultPcmSupport();
setUiModeToTv();
RenderersFactory renderersFactory =
createRenderersFactory(
/* onProcessedOutputBufferRunnable= */ () -> {
// Right after we write the first buffer to audio sink, direct playback support
// is added, and AudioTrack begins to support the audio format directly.
if (!directPlaybackSupportAddedReference.get()) {
addDirectPlaybackSupportForAC3();
directPlaybackSupportAddedReference.set(true);
}
});
ExoPlayer player =
new ExoPlayer.Builder(applicationContext, renderersFactory)
.setClock(new FakeClock(/* isAutoAdvancing= */ true))
.build();
player.setMediaItem(MediaItem.fromUri("asset:///media/mp4/sample_ac3_aac.mp4"));
player.prepare();
Player.Listener listener = mock(Player.Listener.class);
player.addListener(listener);
player.play();
TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_ENDED);
player.release();
// Verify onTracksChanged was called exactly once and the AAC track was selected.
ArgumentCaptor<Tracks> tracks = ArgumentCaptor.forClass(Tracks.class);
verify(listener).onTracksChanged(tracks.capture());
Tracks trackSelection = tracks.getAllValues().get(0);
assertThat(trackSelection.getGroups()).hasSize(2);
assertThat(trackSelection.getGroups().get(0).isSelected()).isFalse();
assertThat(trackSelection.getGroups().get(0).getTrackFormat(0).sampleMimeType)
.isEqualTo(MimeTypes.AUDIO_AC3);
assertThat(trackSelection.getGroups().get(1).isSelected()).isTrue();
assertThat(trackSelection.getGroups().get(1).getTrackFormat(0).sampleMimeType)
.isEqualTo(MimeTypes.AUDIO_AAC);
}
private void setUiModeToTv() {
ShadowUIModeManager shadowUiModeManager =
shadowOf(
(UiModeManager)
ApplicationProvider.getApplicationContext()
.getSystemService(Context.UI_MODE_SERVICE));
shadowUiModeManager.currentModeType = Configuration.UI_MODE_TYPE_TELEVISION;
}
private void addDirectPlaybackSupportForAC3() {
ShadowAudioTrack.addAllowedNonPcmEncoding(ENCODING_AC3);
// Set direct playback support for the format and attributes that AudioCapabilities use when
// querying the platform.
ShadowAudioTrack.addDirectPlaybackSupport(
new AudioFormat.Builder()
.setEncoding(ENCODING_AC3)
.setSampleRate(AudioCapabilities.DEFAULT_SAMPLE_RATE_HZ)
.setChannelMask(CHANNEL_OUT_STEREO)
.build(),
new android.media.AudioAttributes.Builder()
.setUsage(android.media.AudioAttributes.USAGE_MEDIA)
.setContentType(AudioAttributes.CONTENT_TYPE_UNKNOWN)
.setFlags(0)
.build());
directPlaybackDevice = createDirectPlaybackDevice(ENCODING_AC3, CHANNEL_OUT_5POINT1);
if (Util.SDK_INT >= 33) {
shadowOf(audioManager).addOutputDeviceWithDirectProfiles(checkNotNull(directPlaybackDevice));
}
shadowOf(audioManager)
.addOutputDevice(
checkNotNull(directPlaybackDevice), /* notifyAudioDeviceCallbacks= */ true);
}
private void clearDirectPlaybackSupport() {
ShadowAudioTrack.clearAllowedNonPcmEncodings();
ShadowAudioTrack.clearDirectPlaybackSupportedFormats();
if (directPlaybackDevice != null) {
if (Util.SDK_INT >= 33) {
shadowOf(audioManager).removeOutputDeviceWithDirectProfiles(directPlaybackDevice);
}
shadowOf(audioManager)
.removeOutputDevice(directPlaybackDevice, /* notifyAudioDeviceCallbacks= */ true);
directPlaybackDevice = null;
}
}
private void setupDefaultPcmSupport() {
AudioDeviceInfoBuilder defaultDevice =
AudioDeviceInfoBuilder.newBuilder().setType(AudioDeviceInfo.TYPE_BUILTIN_SPEAKER);
if (Util.SDK_INT >= 33) {
defaultDevice.setProfiles(ImmutableList.of(createPcmProfile()));
shadowOf(audioManager).addOutputDeviceWithDirectProfiles(defaultDevice.build());
} else {
shadowOf(audioManager)
.addOutputDevice(defaultDevice.build(), /* notifyAudioDeviceCallbacks= */ true);
}
}
@SuppressWarnings("UseSdkSuppress") // https://issuetracker.google.com/382253664
@RequiresApi(33)
private static AudioProfile createPcmProfile() {
return AudioProfileBuilder.newBuilder()
.setFormat(AudioFormat.ENCODING_PCM_16BIT)
.setSamplingRates(new int[] {48_000})
.setChannelMasks(new int[] {AudioFormat.CHANNEL_OUT_STEREO})
.setChannelIndexMasks(new int[] {})
.setEncapsulationType(AudioProfile.AUDIO_ENCAPSULATION_TYPE_NONE)
.build();
}
private static AudioDeviceInfo createDirectPlaybackDevice(int encoding, int channelMask) {
AudioDeviceInfoBuilder directPlaybackDevice =
AudioDeviceInfoBuilder.newBuilder().setType(AudioDeviceInfo.TYPE_HDMI);
if (Util.SDK_INT >= 33) {
ImmutableList<AudioProfile> expectedProfiles =
ImmutableList.of(
AudioProfileBuilder.newBuilder()
.setFormat(encoding)
.setSamplingRates(new int[] {48_000})
.setChannelMasks(new int[] {channelMask})
.setChannelIndexMasks(new int[] {})
.setEncapsulationType(AudioProfile.AUDIO_ENCAPSULATION_TYPE_NONE)
.build(),
createPcmProfile());
directPlaybackDevice.setProfiles(expectedProfiles);
}
return directPlaybackDevice.build();
}
private RenderersFactory createRenderersFactory(Runnable onProcessedOutputBufferRunnable) {
return (eventHandler,
unusedVideoRendererEventListener,
audioRendererEventListener,
unusedTextRendererOutput,
unusedMetadataRendererOutput) ->
new Renderer[] {
new MediaCodecAudioRenderer(
applicationContext,
new SynchronousMediaCodecAdapter.Factory(),
MediaCodecSelector.DEFAULT,
/* enableDecoderFallback= */ false,
eventHandler,
audioRendererEventListener,
new DefaultAudioSink.Builder(applicationContext).build()) {
@Override
protected void onProcessedOutputBuffer(long presentationTimeUs) {
onProcessedOutputBufferRunnable.run();
super.onProcessedOutputBuffer(presentationTimeUs);
}
}
};
}
private AnalyticsListener createAnalyticsListener() {
return new AnalyticsListener() {
@Override
public void onTracksChanged(EventTime eventTime, Tracks tracks) {
selectedTracks.add(tracks);
}
@Override
public void onAudioEnabled(EventTime eventTime, DecoderCounters decoderCounters) {
analyticsListenerReceivedCallbacks.add("onAudioEnabled");
}
@Override
public void onAudioDisabled(EventTime eventTime, DecoderCounters decoderCounters) {
analyticsListenerReceivedCallbacks.add("onAudioDisabled");
}
@Override
public void onAudioDecoderInitialized(
EventTime eventTime,
String decoderName,
long initializedTimestampMs,
long initializationDurationMs) {
analyticsListenerReceivedCallbacks.add("onAudioDecoderInitialized");
}
@Override
public void onAudioDecoderReleased(EventTime eventTime, String decoderName) {
analyticsListenerReceivedCallbacks.add("onAudioDecoderReleased");
}
};
}
}

View File

@ -0,0 +1,529 @@
/*
* 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 android.media.AudioFormat.CHANNEL_OUT_5POINT1;
import static androidx.media3.common.util.Assertions.checkNotNull;
import static androidx.media3.exoplayer.audio.AudioCapabilities.ALL_SURROUND_ENCODINGS_AND_MAX_CHANNELS;
import static androidx.media3.exoplayer.audio.AudioCapabilities.getCapabilities;
import static com.google.common.truth.Truth.assertThat;
import static org.robolectric.Shadows.shadowOf;
import android.app.UiModeManager;
import android.content.Context;
import android.content.Intent;
import android.content.res.Configuration;
import android.media.AudioDeviceInfo;
import android.media.AudioFormat;
import android.media.AudioManager;
import android.media.AudioProfile;
import android.provider.Settings.Global;
import android.util.Pair;
import androidx.annotation.RequiresApi;
import androidx.media3.common.AudioAttributes;
import androidx.media3.common.C;
import androidx.media3.common.Format;
import androidx.media3.common.MimeTypes;
import androidx.media3.common.util.Util;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.common.collect.ImmutableList;
import java.util.ArrayList;
import java.util.List;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.annotation.Config;
import org.robolectric.shadows.AudioDeviceInfoBuilder;
import org.robolectric.shadows.AudioProfileBuilder;
import org.robolectric.shadows.ShadowAudioTrack;
import org.robolectric.shadows.ShadowBuild;
import org.robolectric.shadows.ShadowUIModeManager;
/** Unit tests for {@link AudioCapabilities}. */
@RunWith(AndroidJUnit4.class)
public class AudioCapabilitiesTest {
private AudioManager audioManager;
private UiModeManager uiModeManager;
@Before
public void setUp() {
audioManager =
(AudioManager)
ApplicationProvider.getApplicationContext().getSystemService(Context.AUDIO_SERVICE);
shadowOf(audioManager).setOutputDevices(ImmutableList.of());
uiModeManager =
(UiModeManager)
ApplicationProvider.getApplicationContext().getSystemService(Context.UI_MODE_SERVICE);
}
@Test
@Config(minSdk = 33)
public void getCapabilities_returnsCapabilitiesFromDirectProfiles_onTvV33() {
// Set UI mode to TV.
ShadowUIModeManager shadowUiModeManager =
shadowOf(
(UiModeManager)
ApplicationProvider.getApplicationContext()
.getSystemService(Context.UI_MODE_SERVICE));
shadowUiModeManager.currentModeType = Configuration.UI_MODE_TYPE_TELEVISION;
int[] channelMasks =
new int[] {
AudioFormat.CHANNEL_OUT_MONO,
AudioFormat.CHANNEL_OUT_STEREO,
AudioFormat.CHANNEL_OUT_STEREO | AudioFormat.CHANNEL_OUT_FRONT_CENTER,
AudioFormat.CHANNEL_OUT_QUAD,
AudioFormat.CHANNEL_OUT_QUAD | AudioFormat.CHANNEL_OUT_FRONT_CENTER,
AudioFormat.CHANNEL_OUT_5POINT1
};
ImmutableList<AudioProfile> expectedProfiles =
ImmutableList.of(
AudioProfileBuilder.newBuilder()
.setFormat(AudioFormat.ENCODING_DTS)
.setSamplingRates(new int[] {44_100, 48_000})
.setChannelMasks(channelMasks)
.setChannelIndexMasks(new int[] {})
.setEncapsulationType(AudioProfile.AUDIO_ENCAPSULATION_TYPE_NONE)
.build(),
AudioProfileBuilder.newBuilder()
.setFormat(AudioFormat.ENCODING_PCM_16BIT)
.setSamplingRates(new int[] {48_000})
.setChannelMasks(new int[] {AudioFormat.CHANNEL_OUT_STEREO})
.setChannelIndexMasks(new int[] {})
.setEncapsulationType(AudioProfile.AUDIO_ENCAPSULATION_TYPE_NONE)
.build());
AudioDeviceInfo device =
AudioDeviceInfoBuilder.newBuilder()
.setType(AudioDeviceInfo.TYPE_HDMI)
.setProfiles(expectedProfiles)
.build();
shadowOf(audioManager).addOutputDeviceWithDirectProfiles(device);
AudioCapabilities audioCapabilities =
AudioCapabilities.getCapabilities(
ApplicationProvider.getApplicationContext(),
AudioAttributes.DEFAULT,
/* routedDevice= */ null);
assertThat(audioCapabilities.supportsEncoding(C.ENCODING_PCM_16BIT)).isTrue();
for (int encoding : ALL_SURROUND_ENCODINGS_AND_MAX_CHANNELS.keySet()) {
if (encoding == AudioFormat.ENCODING_DTS) {
assertThat(audioCapabilities.supportsEncoding(encoding)).isTrue();
} else {
// Should not support all the other encodings.
assertThat(audioCapabilities.supportsEncoding(encoding)).isFalse();
}
}
// Should not support the format whose channel count is not reported in the profile.
assertThat(
audioCapabilities.isPassthroughPlaybackSupported(
new Format.Builder()
.setSampleMimeType(MimeTypes.AUDIO_DTS)
.setChannelCount(8)
.build(),
AudioAttributes.DEFAULT))
.isFalse();
}
/** {@link AudioDeviceInfo#TYPE_BLUETOOTH_A2DP} is only supported from API 23. */
@Test
@Config(minSdk = 23)
public void getCapabilities_withBluetoothA2dpAndHdmiConnectedApi23_returnsDefaultCapabilities() {
setOutputDevices(AudioDeviceInfo.TYPE_BLUETOOTH_A2DP, AudioDeviceInfo.TYPE_HDMI);
configureHdmiConnection(/* maxChannelCount= */ 6, /* encodings...= */ AudioFormat.ENCODING_AC3);
AudioDeviceInfo[] audioDeviceInfos =
shadowOf(audioManager).getDevices(AudioManager.GET_DEVICES_OUTPUTS);
AudioCapabilities audioCapabilities =
AudioCapabilities.getCapabilities(
ApplicationProvider.getApplicationContext(),
AudioAttributes.DEFAULT,
/* routedDevice= */ null);
assertThat(getDeviceTypes(audioDeviceInfos))
.containsAtLeast(AudioDeviceInfo.TYPE_BLUETOOTH_A2DP, AudioDeviceInfo.TYPE_HDMI);
assertThat(audioCapabilities).isEqualTo(AudioCapabilities.DEFAULT_AUDIO_CAPABILITIES);
}
/** {@link AudioDeviceInfo#TYPE_BLE_HEADSET} is only supported from API 31. */
@Test
@Config(minSdk = 31)
public void
getCapabilities_withBluetoothHeadsetAndHmdiConnectedApi31_returnsDefaultCapabilities() {
setOutputDevices(AudioDeviceInfo.TYPE_BLE_HEADSET, AudioDeviceInfo.TYPE_HDMI);
configureHdmiConnection(/* maxChannelCount= */ 6, /* encodings...= */ AudioFormat.ENCODING_AC3);
AudioDeviceInfo[] audioDeviceInfos =
shadowOf(audioManager).getDevices(AudioManager.GET_DEVICES_OUTPUTS);
AudioCapabilities audioCapabilities =
AudioCapabilities.getCapabilities(
ApplicationProvider.getApplicationContext(),
AudioAttributes.DEFAULT,
/* routedDevice= */ null);
assertThat(getDeviceTypes(audioDeviceInfos))
.containsAtLeast(AudioDeviceInfo.TYPE_BLE_HEADSET, AudioDeviceInfo.TYPE_HDMI);
assertThat(audioCapabilities).isEqualTo(AudioCapabilities.DEFAULT_AUDIO_CAPABILITIES);
}
/** {@link AudioDeviceInfo#TYPE_BLE_BROADCAST} is only supported from API 33. */
@Test
@Config(minSdk = 33)
public void
getCapabilities_withBluetoothBroadcastAndHdmiConnectedApi33_returnsDefaultCapabilities() {
setOutputDevices(AudioDeviceInfo.TYPE_BLE_BROADCAST, AudioDeviceInfo.TYPE_HDMI);
configureHdmiConnection(/* maxChannelCount= */ 6, /* encodings...= */ AudioFormat.ENCODING_AC3);
AudioDeviceInfo[] audioDeviceInfos =
shadowOf(audioManager).getDevices(AudioManager.GET_DEVICES_OUTPUTS);
AudioCapabilities audioCapabilities =
AudioCapabilities.getCapabilities(
ApplicationProvider.getApplicationContext(),
AudioAttributes.DEFAULT,
/* routedDevice= */ null);
assertThat(getDeviceTypes(audioDeviceInfos))
.containsAtLeast(AudioDeviceInfo.TYPE_BLE_BROADCAST, AudioDeviceInfo.TYPE_HDMI);
assertThat(audioCapabilities).isEqualTo(AudioCapabilities.DEFAULT_AUDIO_CAPABILITIES);
}
@Test
public void getCapabilities_noBluetoothButHdmiConnected_returnsHdmiCapabilities() {
setOutputDevices(AudioDeviceInfo.TYPE_HDMI);
configureHdmiConnection(
/* maxChannelCount= */ 6,
/* encodings...= */ AudioFormat.ENCODING_AC3,
AudioFormat.ENCODING_DTS,
AudioFormat.ENCODING_E_AC3);
AudioDeviceInfo[] audioDeviceInfos =
shadowOf(audioManager).getDevices(AudioManager.GET_DEVICES_OUTPUTS);
AudioCapabilities audioCapabilities =
AudioCapabilities.getCapabilities(
ApplicationProvider.getApplicationContext(),
AudioAttributes.DEFAULT,
/* routedDevice= */ null);
assertThat(getDeviceTypes(audioDeviceInfos)).contains(AudioDeviceInfo.TYPE_HDMI);
assertThat(getDeviceTypes(audioDeviceInfos))
.doesNotContain(AudioDeviceInfo.TYPE_BLUETOOTH_A2DP);
assertThat(audioCapabilities)
.isEqualTo(
new AudioCapabilities(
new int[] {
AudioFormat.ENCODING_PCM_16BIT,
AudioFormat.ENCODING_AC3,
AudioFormat.ENCODING_DTS,
AudioFormat.ENCODING_E_AC3
},
/* maxChannelCount= */ 6));
}
@Config(maxSdk = 32) // Fallback test for APIs before 33
@Test
public void
getCapabilities_noBluetoothButGlobalSurroundSettingOnAmazon_returnsExternalSurroundCapabilities() {
Global.putInt(
ApplicationProvider.getApplicationContext().getContentResolver(),
"external_surround_sound_enabled",
1);
ShadowBuild.setManufacturer("Amazon");
AudioCapabilities audioCapabilities =
AudioCapabilities.getCapabilities(
ApplicationProvider.getApplicationContext(),
AudioAttributes.DEFAULT,
/* routedDevice= */ null);
assertThat(audioCapabilities.supportsEncoding(C.ENCODING_PCM_16BIT)).isTrue();
assertThat(audioCapabilities.supportsEncoding(C.ENCODING_AC3)).isTrue();
assertThat(audioCapabilities.supportsEncoding(C.ENCODING_E_AC3)).isTrue();
}
// Fallback test for APIs before 33, TYPE_HDMI is only supported from API 23
@Config(minSdk = 23, maxSdk = 32)
@Test
public void
getCapabilities_noBluetoothButGlobalSurroundSettingForced_returnsExternalSurroundCapabilitiesAndIgnoresHdmi() {
setOutputDevices(AudioDeviceInfo.TYPE_HDMI);
configureHdmiConnection(/* maxChannelCount= */ 6, /* encodings...= */ AudioFormat.ENCODING_DTS);
Global.putInt(
ApplicationProvider.getApplicationContext().getContentResolver(),
"use_external_surround_sound_flag",
1);
AudioCapabilities audioCapabilitiesWithoutFlag =
AudioCapabilities.getCapabilities(
ApplicationProvider.getApplicationContext(),
AudioAttributes.DEFAULT,
/* routedDevice= */ null);
Global.putInt(
ApplicationProvider.getApplicationContext().getContentResolver(),
"external_surround_sound_enabled",
1);
AudioCapabilities audioCapabilitiesWithFlag =
AudioCapabilities.getCapabilities(
ApplicationProvider.getApplicationContext(),
AudioAttributes.DEFAULT,
/* routedDevice= */ null);
assertThat(audioCapabilitiesWithoutFlag.supportsEncoding(C.ENCODING_PCM_16BIT)).isTrue();
assertThat(audioCapabilitiesWithoutFlag.supportsEncoding(C.ENCODING_AC3)).isFalse();
assertThat(audioCapabilitiesWithoutFlag.supportsEncoding(C.ENCODING_E_AC3)).isFalse();
assertThat(audioCapabilitiesWithoutFlag.supportsEncoding(C.ENCODING_DTS)).isFalse();
assertThat(audioCapabilitiesWithFlag.supportsEncoding(C.ENCODING_PCM_16BIT)).isTrue();
assertThat(audioCapabilitiesWithFlag.supportsEncoding(C.ENCODING_AC3)).isTrue();
assertThat(audioCapabilitiesWithFlag.supportsEncoding(C.ENCODING_E_AC3)).isTrue();
assertThat(audioCapabilitiesWithFlag.supportsEncoding(C.ENCODING_DTS)).isFalse();
}
@Test
@Config(minSdk = 23) // TYPE_BLUETOOTH_A2DP detection is supported from API 23.
public void
getCapabilities_withBluetoothA2dpConnectedAndHdmiAsRoutedDeviceHintApi23_returnsHdmiCapabilities() {
setOutputDevices(AudioDeviceInfo.TYPE_BLUETOOTH_A2DP, AudioDeviceInfo.TYPE_HDMI);
configureHdmiConnection(
/* maxChannelCount= */ 10,
/* encodings...= */ AudioFormat.ENCODING_AC3,
AudioFormat.ENCODING_DTS,
AudioFormat.ENCODING_E_AC3_JOC);
AudioDeviceInfo[] audioDeviceInfos =
shadowOf(audioManager).getDevices(AudioManager.GET_DEVICES_OUTPUTS);
AudioDeviceInfo routedDevice = null;
for (AudioDeviceInfo deviceInfo : audioDeviceInfos) {
if (deviceInfo.getType() == AudioDeviceInfo.TYPE_HDMI) {
routedDevice = deviceInfo;
}
}
AudioCapabilities audioCapabilities =
AudioCapabilities.getCapabilities(
ApplicationProvider.getApplicationContext(), AudioAttributes.DEFAULT, routedDevice);
assertThat(routedDevice).isNotNull();
assertThat(getDeviceTypes(audioDeviceInfos))
.containsAtLeast(AudioDeviceInfo.TYPE_BLUETOOTH_A2DP, AudioDeviceInfo.TYPE_HDMI);
assertThat(audioCapabilities)
.isEqualTo(
new AudioCapabilities(
new int[] {
AudioFormat.ENCODING_PCM_16BIT,
AudioFormat.ENCODING_AC3,
AudioFormat.ENCODING_DTS,
AudioFormat.ENCODING_E_AC3_JOC
},
/* maxChannelCount= */ 10));
}
@Test
@Config(minSdk = 33) // getAudioDevicesForAttributes only works from API33
public void
getCapabilities_withBluetoothA2dpAndHdmiConnectedAndHdmiAsDefaultRoutedDeviceApi33_returnsHdmiCapabilities() {
setOutputDevices(AudioDeviceInfo.TYPE_HDMI, AudioDeviceInfo.TYPE_BLUETOOTH_A2DP);
setDefaultRoutedDevice(AudioAttributes.DEFAULT, AudioDeviceInfo.TYPE_HDMI);
configureHdmiConnection(
/* maxChannelCount= */ 10,
/* encodings...= */ AudioFormat.ENCODING_AC3,
AudioFormat.ENCODING_DTS,
AudioFormat.ENCODING_E_AC3_JOC);
AudioDeviceInfo[] audioDeviceInfos =
shadowOf(audioManager).getDevices(AudioManager.GET_DEVICES_OUTPUTS);
AudioCapabilities audioCapabilities =
AudioCapabilities.getCapabilities(
ApplicationProvider.getApplicationContext(),
AudioAttributes.DEFAULT,
/* routedDevice= */ null);
assertThat(getDeviceTypes(audioDeviceInfos))
.containsAtLeast(AudioDeviceInfo.TYPE_BLUETOOTH_A2DP, AudioDeviceInfo.TYPE_HDMI);
assertThat(audioCapabilities)
.isEqualTo(
new AudioCapabilities(
new int[] {
AudioFormat.ENCODING_PCM_16BIT,
AudioFormat.ENCODING_AC3,
AudioFormat.ENCODING_DTS,
AudioFormat.ENCODING_E_AC3_JOC
},
/* maxChannelCount= */ 10));
}
@Test
public void getCapabilities_noExternalOutputs_notTvNorAutomotive_returnsDefaultCapabilities() {
AudioCapabilities audioCapabilities =
AudioCapabilities.getCapabilities(
ApplicationProvider.getApplicationContext(),
AudioAttributes.DEFAULT,
/* routedDevice= */ null);
assertThat(audioCapabilities).isEqualTo(AudioCapabilities.DEFAULT_AUDIO_CAPABILITIES);
}
@Config(minSdk = 29)
@Test
public void
getEncodingAndChannelConfigForPassthrough_forEAc3JocAndSingleSupportedConfig_returnsCorrectEncodingAndChannelConfig() {
// Set UI mode to TV.
shadowOf(uiModeManager).currentModeType = Configuration.UI_MODE_TYPE_TELEVISION;
Format format =
new Format.Builder()
.setSampleMimeType(MimeTypes.AUDIO_E_AC3_JOC)
.setSampleRate(48_000)
.build();
AudioAttributes directPlaybackAudioAttributes =
new AudioAttributes.Builder()
.setUsage(C.USAGE_MEDIA)
.setContentType(C.AUDIO_CONTENT_TYPE_MUSIC)
.build();
addDirectPlaybackSupport(
AudioFormat.ENCODING_E_AC3_JOC, CHANNEL_OUT_5POINT1, directPlaybackAudioAttributes);
AudioCapabilities audioCapabilities =
getCapabilities(
ApplicationProvider.getApplicationContext(),
directPlaybackAudioAttributes,
/* routedDevice= */ null);
Pair<Integer, Integer> encodingAndChannelConfig =
audioCapabilities.getEncodingAndChannelConfigForPassthrough(
format, directPlaybackAudioAttributes);
assertThat(encodingAndChannelConfig.first).isEqualTo(AudioFormat.ENCODING_E_AC3_JOC);
assertThat(encodingAndChannelConfig.second).isEqualTo(AudioFormat.CHANNEL_OUT_5POINT1);
}
// TODO: b/320191198 - Disable the test for API 33, as the
// ShadowAudioManager.getDirectProfilesForAttributes(AudioAttributes) hasn't really considered
// the AudioAttributes yet.
@Config(minSdk = 29, maxSdk = 32)
@Test
public void
getEncodingAndChannelConfigForPassthrough_forDifferentAudioAttributes_returnsUnsupported() {
// Set UI mode to TV.
shadowOf(uiModeManager).currentModeType = Configuration.UI_MODE_TYPE_TELEVISION;
Format format =
new Format.Builder()
.setSampleMimeType(MimeTypes.AUDIO_E_AC3_JOC)
.setSampleRate(48_000)
.build();
AudioAttributes directPlaybackAudioAttributes =
new AudioAttributes.Builder()
.setUsage(C.USAGE_MEDIA)
.setContentType(C.AUDIO_CONTENT_TYPE_MUSIC)
.build();
addDirectPlaybackSupport(
AudioFormat.ENCODING_E_AC3_JOC, CHANNEL_OUT_5POINT1, directPlaybackAudioAttributes);
AudioCapabilities audioCapabilities =
getCapabilities(
ApplicationProvider.getApplicationContext(),
directPlaybackAudioAttributes,
/* routedDevice= */ null);
AudioAttributes audioAttributes =
new AudioAttributes.Builder()
.setUsage(C.USAGE_MEDIA)
.setContentType(C.AUDIO_CONTENT_TYPE_MOVIE)
.build();
Pair<Integer, Integer> encodingAndChannelConfig =
audioCapabilities.getEncodingAndChannelConfigForPassthrough(format, audioAttributes);
assertThat(encodingAndChannelConfig).isNull();
}
/**
* Sets all the available output devices and uses the first as the default routed device for the
* given {@link AudioAttributes}
*/
private void setOutputDevices(int... types) {
ImmutableList.Builder<AudioDeviceInfo> audioDeviceInfos = ImmutableList.builder();
for (int type : types) {
audioDeviceInfos.add(AudioDeviceInfoBuilder.newBuilder().setType(type).build());
}
shadowOf(audioManager).setOutputDevices(audioDeviceInfos.build());
}
@SuppressWarnings("UseSdkSuppress") // https://issuetracker.google.com/382253664
@RequiresApi(33)
private void setDefaultRoutedDevice(AudioAttributes audioAttributes, int type) {
shadowOf(audioManager)
.setAudioDevicesForAttributes(
audioAttributes.getAudioAttributesV21().audioAttributes,
ImmutableList.of(AudioDeviceInfoBuilder.newBuilder().setType(type).build()));
}
@SuppressWarnings("UseSdkSuppress") // https://issuetracker.google.com/382253664
@RequiresApi(23)
private void addDirectPlaybackSupport(
int encoding, int channelMask, AudioAttributes audioAttributes) {
ShadowAudioTrack.addAllowedNonPcmEncoding(AudioFormat.ENCODING_E_AC3_JOC);
// We have to add a support for STEREO channel mask as
// Api29.getDirectPlaybackSupportedEncodings in AudioCapabilities uses CHANNEL_OUT_STEREO
// to query the support from the platform.
ShadowAudioTrack.addDirectPlaybackSupport(
new AudioFormat.Builder()
.setEncoding(encoding)
.setSampleRate(48_000)
.setChannelMask(AudioFormat.CHANNEL_OUT_STEREO)
.build(),
audioAttributes.getAudioAttributesV21().audioAttributes);
ShadowAudioTrack.addDirectPlaybackSupport(
new AudioFormat.Builder()
.setEncoding(encoding)
.setSampleRate(48_000)
.setChannelMask(channelMask)
.build(),
audioAttributes.getAudioAttributesV21().audioAttributes);
AudioDeviceInfoBuilder deviceInfoBuilder =
AudioDeviceInfoBuilder.newBuilder().setType(AudioDeviceInfo.TYPE_HDMI);
if (Util.SDK_INT >= 33) {
ImmutableList<AudioProfile> expectedProfiles =
ImmutableList.of(
AudioProfileBuilder.newBuilder()
.setFormat(encoding)
.setSamplingRates(new int[] {48_000})
.setChannelMasks(new int[] {channelMask})
.setChannelIndexMasks(new int[] {})
.setEncapsulationType(AudioProfile.AUDIO_ENCAPSULATION_TYPE_NONE)
.build());
deviceInfoBuilder.setProfiles(expectedProfiles);
}
AudioDeviceInfo directPlaybackDevice = deviceInfoBuilder.build();
shadowOf(audioManager)
.addOutputDevice(directPlaybackDevice, /* notifyAudioDeviceCallbacks= */ true);
shadowOf(audioManager).addOutputDeviceWithDirectProfiles(checkNotNull(directPlaybackDevice));
}
// Adding the permission to the test AndroidManifest.xml doesn't work to appease lint.
@SuppressWarnings({"StickyBroadcast", "MissingPermission"})
private void configureHdmiConnection(int maxChannelCount, int... encodings) {
Intent intent = new Intent(AudioManager.ACTION_HDMI_AUDIO_PLUG);
intent.putExtra(AudioManager.EXTRA_AUDIO_PLUG_STATE, 1);
intent.putExtra(AudioManager.EXTRA_ENCODINGS, encodings);
intent.putExtra(AudioManager.EXTRA_MAX_CHANNEL_COUNT, maxChannelCount);
ApplicationProvider.getApplicationContext().sendStickyBroadcast(intent);
}
@SuppressWarnings("UseSdkSuppress") // https://issuetracker.google.com/382253664
@RequiresApi(23)
private List<Integer> getDeviceTypes(AudioDeviceInfo[] audioDeviceInfos) {
List<Integer> deviceTypes = new ArrayList<>();
for (AudioDeviceInfo audioDeviceInfo : audioDeviceInfos) {
deviceTypes.add(audioDeviceInfo.getType());
}
return deviceTypes;
}
}

View File

@ -0,0 +1,121 @@
/*
* 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.media3.common.TrackSelectionParameters.AudioOffloadPreferences.AUDIO_OFFLOAD_MODE_ENABLED;
import static androidx.test.core.app.ApplicationProvider.getApplicationContext;
import static com.google.common.truth.Truth.assertThat;
import android.content.Context;
import android.media.AudioAttributes;
import android.media.AudioFormat;
import android.media.AudioManager;
import androidx.media3.common.MediaItem;
import androidx.media3.common.Player;
import androidx.media3.common.TrackSelectionParameters.AudioOffloadPreferences;
import androidx.media3.exoplayer.DefaultRenderersFactory;
import androidx.media3.exoplayer.ExoPlayer;
import androidx.media3.exoplayer.ExoPlayer.AudioOffloadListener;
import androidx.media3.test.utils.FakeClock;
import androidx.media3.test.utils.robolectric.ShadowMediaCodecConfig;
import androidx.media3.test.utils.robolectric.TestPlayerRunHelper;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import java.util.concurrent.atomic.AtomicBoolean;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.annotation.Config;
import org.robolectric.shadows.ShadowAudioSystem;
@RunWith(AndroidJUnit4.class)
public class AudioOffloadEndToEndTest {
private static final AudioFormat AUDIO_FORMAT =
new AudioFormat.Builder()
.setSampleRate(48_000)
.setChannelMask(AudioFormat.CHANNEL_OUT_STEREO)
.setEncoding(AudioFormat.ENCODING_OPUS)
.build();
private static final AudioAttributes AUDIO_ATTRIBUTES =
new AudioAttributes.Builder()
.setContentType(AudioAttributes.CONTENT_TYPE_UNKNOWN)
.setUsage(AudioAttributes.USAGE_MEDIA)
.setAllowedCapturePolicy(AudioAttributes.ALLOW_CAPTURE_BY_ALL)
.build();
private static final String OPUS_FILE_URI = "asset:///media/ogg/bear.opus";
private final Context applicationContext = getApplicationContext();
@Rule
public ShadowMediaCodecConfig mediaCodecConfig =
ShadowMediaCodecConfig.forAllSupportedMimeTypes();
@Before
public void setup() {
ShadowAudioSystem.setOffloadSupported(AUDIO_FORMAT, AUDIO_ATTRIBUTES, true);
ShadowAudioSystem.setOffloadPlaybackSupport(
AUDIO_FORMAT, AUDIO_ATTRIBUTES, AudioManager.PLAYBACK_OFFLOAD_SUPPORTED);
ShadowAudioSystem.setDirectPlaybackSupport(
AUDIO_FORMAT, AUDIO_ATTRIBUTES, AudioManager.DIRECT_PLAYBACK_OFFLOAD_SUPPORTED);
}
@Test
@Config(minSdk = 30)
public void testOffloadPlayback_offloadEnabledAndPlayToEnd() throws Exception {
AtomicBoolean isOffloadModeSet = new AtomicBoolean(false);
DefaultRenderersFactory renderersFactory =
new DefaultRenderersFactory(applicationContext) {
@Override
protected AudioSink buildAudioSink(
Context context, boolean enableFloatOutput, boolean enableAudioTrackPlaybackParams) {
AudioOffloadListener audioOffloadListener =
new AudioOffloadListener() {
@Override
public void onOffloadedPlayback(boolean offloadedPlayback) {
isOffloadModeSet.set(offloadedPlayback);
}
};
return new DefaultAudioSink.Builder(applicationContext)
.setEnableFloatOutput(enableFloatOutput)
.setEnableAudioTrackPlaybackParams(enableAudioTrackPlaybackParams)
.setExperimentalAudioOffloadListener(audioOffloadListener)
.build();
}
};
ExoPlayer player =
new ExoPlayer.Builder(applicationContext, renderersFactory)
.setClock(new FakeClock(/* isAutoAdvancing= */ true))
.build();
player.setTrackSelectionParameters(
player
.getTrackSelectionParameters()
.buildUpon()
.setAudioOffloadPreferences(
new AudioOffloadPreferences.Builder()
.setAudioOffloadMode(AUDIO_OFFLOAD_MODE_ENABLED)
.build())
.build());
player.setMediaItem(MediaItem.fromUri(OPUS_FILE_URI));
player.prepare();
player.play();
TestPlayerRunHelper.runUntilPlaybackState(player, Player.STATE_ENDED);
assertThat(isOffloadModeSet.get()).isTrue();
player.release();
}
}

View File

@ -20,27 +20,49 @@ import static androidx.media3.exoplayer.audio.AudioSink.SINK_FORMAT_SUPPORTED_DI
import static androidx.media3.exoplayer.audio.AudioSink.SINK_FORMAT_SUPPORTED_WITH_TRANSCODING;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertThrows;
import static org.robolectric.Shadows.shadowOf;
import android.app.UiModeManager;
import android.content.Context;
import android.content.Intent;
import android.content.res.Configuration;
import android.media.AudioDeviceInfo;
import android.media.AudioFormat;
import android.media.AudioManager;
import android.media.AudioProfile;
import android.os.Looper;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.media3.common.AudioAttributes;
import androidx.media3.common.C;
import androidx.media3.common.Format;
import androidx.media3.common.MimeTypes;
import androidx.media3.common.PlaybackParameters;
import androidx.media3.common.audio.AudioProcessor;
import androidx.media3.common.audio.AudioProcessorChain;
import androidx.media3.common.util.Util;
import androidx.media3.exoplayer.audio.DefaultAudioSink.DefaultAudioProcessorChain;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.common.collect.ImmutableList;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.Arrays;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.annotation.Config;
import org.robolectric.shadows.AudioDeviceInfoBuilder;
import org.robolectric.shadows.AudioProfileBuilder;
import org.robolectric.shadows.ShadowAudioManager;
import org.robolectric.shadows.ShadowAudioTrack;
import org.robolectric.shadows.ShadowLooper;
import org.robolectric.shadows.ShadowSystemClock;
import org.robolectric.shadows.ShadowUIModeManager;
/** Unit tests for {@link DefaultAudioSink}. */
@RunWith(AndroidJUnit4.class)
@ -50,6 +72,7 @@ public final class DefaultAudioSinkTest {
private static final int CHANNEL_COUNT_MONO = 1;
private static final int CHANNEL_COUNT_STEREO = 2;
private static final int DEFAULT_MAX_CHANNEL_COUNT = 8;
private static final int BYTES_PER_FRAME_16_BIT = 2;
private static final int SAMPLE_RATE_44_1 = 44100;
private static final int TRIM_100_MS_FRAME_COUNT = 4410;
@ -63,6 +86,9 @@ public final class DefaultAudioSinkTest {
private DefaultAudioSink defaultAudioSink;
private ArrayAudioBufferSink arrayAudioBufferSink;
@Nullable private AudioDeviceInfo hdmiDevice;
@Nullable private AudioDeviceInfo bluetoothDevice;
@Before
public void setUp() {
// For capturing output.
@ -74,6 +100,12 @@ public final class DefaultAudioSinkTest {
.build();
}
@After
public void tearDown() {
removeBluetoothDevice();
removeHdmiDevice();
}
@Test
public void handlesSpecializedAudioProcessorArray() {
defaultAudioSink =
@ -359,6 +391,331 @@ public final class DefaultAudioSinkTest {
assertThat(defaultAudioSink.supportsFormat(aacLcFormat)).isFalse();
}
@Test
@Config(minSdk = 23)
public void audioSinkWithNonNullContext_audioCapabilitiesObtainedFromContext() {
// Set UI mode to TV.
getShadowUiModeManager().currentModeType = Configuration.UI_MODE_TYPE_TELEVISION;
addHdmiDevice();
DefaultAudioSink audioSink =
new DefaultAudioSink.Builder(ApplicationProvider.getApplicationContext()).build();
assertThat(
audioSink.supportsFormat(
new Format.Builder()
.setSampleMimeType(MimeTypes.AUDIO_DTS_HD)
.setChannelCount(DEFAULT_MAX_CHANNEL_COUNT)
.build()))
.isTrue();
}
@Test
@SuppressWarnings("deprecation") // Testing deprecated builder methods.
public void audioSinkWithNullContext_audioCapabilitiesObtainedFromBuilder() {
DefaultAudioSink audioSink =
new DefaultAudioSink.Builder()
.setAudioCapabilities(
new AudioCapabilities(
new int[] {MimeTypes.getEncoding(MimeTypes.AUDIO_DTS_HD, /* codec= */ null)},
DEFAULT_MAX_CHANNEL_COUNT))
.build();
assertThat(
audioSink.supportsFormat(
new Format.Builder()
.setSampleMimeType(MimeTypes.AUDIO_DTS_HD)
.setChannelCount(DEFAULT_MAX_CHANNEL_COUNT)
.build()))
.isTrue();
}
@Test
@SuppressWarnings("deprecation") // Testing deprecated builder methods.
public void audioSinkWithNullContext_audioCapabilitiesObtainedFromBuilder_defaultCapabilities() {
DefaultAudioSink audioSink = new DefaultAudioSink.Builder().build();
assertThat(
audioSink.supportsFormat(
new Format.Builder()
.setSampleMimeType(MimeTypes.AUDIO_DTS_HD)
.setChannelCount(DEFAULT_MAX_CHANNEL_COUNT)
.build()))
.isFalse();
}
@Test
@Config(minSdk = 23) // AudioManager.TYPE_BLUETOOTH_A2DP is supported from API 23.
public void bluetoothDeviceAddedAndRemoved_audioCapabilitiesUpdated() {
// Set UI mode to TV.
getShadowUiModeManager().currentModeType = Configuration.UI_MODE_TYPE_TELEVISION;
// Initially setup the audio sink with HDMI device connected.
addHdmiDevice();
DefaultAudioSink audioSink =
new DefaultAudioSink.Builder(ApplicationProvider.getApplicationContext()).build();
assertThat(
audioSink.supportsFormat(
new Format.Builder()
.setSampleMimeType(MimeTypes.AUDIO_DTS_HD)
.setChannelCount(DEFAULT_MAX_CHANNEL_COUNT)
.build()))
.isTrue();
// Add a bluetooth device.
addBluetoothDevice();
// When the bluetooth device is connected, the audio sink should change to the default PCM
// capabilities, thus the surrounded format shouldn't be reported to support.
assertThat(
audioSink.supportsFormat(
new Format.Builder()
.setSampleMimeType(MimeTypes.AUDIO_DTS_HD)
.setChannelCount(DEFAULT_MAX_CHANNEL_COUNT)
.build()))
.isFalse();
// Remove the bluetooth device.
removeBluetoothDevice();
// When the bluetooth device is disconnected, the audio sink should change to the capabilities
// reported by the HDMI device, as that device is still connected.
assertThat(
audioSink.supportsFormat(
new Format.Builder()
.setSampleMimeType(MimeTypes.AUDIO_DTS_HD)
.setChannelCount(DEFAULT_MAX_CHANNEL_COUNT)
.build()))
.isTrue();
}
@Test
@Config(minSdk = 21) // AudioManager.ACTION_HDMI_AUDIO_PLUG is supported from API 21.
public void hdmiDeviceAddedAndRemoved_audioCapabilitiesUpdated() {
// Set UI mode to TV.
getShadowUiModeManager().currentModeType = Configuration.UI_MODE_TYPE_TELEVISION;
// Initially setup the audio sink.
DefaultAudioSink audioSink =
new DefaultAudioSink.Builder(ApplicationProvider.getApplicationContext()).build();
assertThat(
audioSink.supportsFormat(
new Format.Builder()
.setSampleMimeType(MimeTypes.AUDIO_DTS_HD)
.setChannelCount(DEFAULT_MAX_CHANNEL_COUNT)
.build()))
.isFalse();
// Add an HDMI device.
addHdmiDevice();
// When the HDMI device is connected, the audio sink should change to the capabilities reported
// by the HDMI device.
assertThat(
audioSink.supportsFormat(
new Format.Builder()
.setSampleMimeType(MimeTypes.AUDIO_DTS_HD)
.setChannelCount(DEFAULT_MAX_CHANNEL_COUNT)
.build()))
.isTrue();
// Remove the HDMI device.
removeHdmiDevice();
// When the HDMI device is disconnected, the audio sink should change to the default PCM
// capabilities. We are verifying the surround format reported by the HDMI device before is no
// longer supported.
assertThat(
audioSink.supportsFormat(
new Format.Builder()
.setSampleMimeType(MimeTypes.AUDIO_DTS_HD)
.setChannelCount(DEFAULT_MAX_CHANNEL_COUNT)
.build()))
.isFalse();
}
// TODO: b/320191198 - Disable the test for API 33, as the
// ShadowAudioManager.getDirectProfilesForAttributes(AudioAttributes) hasn't really considered
// the AudioAttributes yet.
@Test
@Config(minSdk = 29, maxSdk = 32)
public void setAudioAttributes_audioCapabilitiesUpdated() {
Context context = ApplicationProvider.getApplicationContext();
getShadowUiModeManager().setCurrentModeType(Configuration.UI_MODE_TYPE_TELEVISION);
ShadowAudioTrack.addDirectPlaybackSupport(
new AudioFormat.Builder()
.setEncoding(AudioFormat.ENCODING_E_AC3_JOC)
.setSampleRate(48_000)
.setChannelMask(AudioFormat.CHANNEL_OUT_STEREO)
.build(),
new android.media.AudioAttributes.Builder()
.setUsage(android.media.AudioAttributes.USAGE_MEDIA)
.setContentType(android.media.AudioAttributes.CONTENT_TYPE_MUSIC)
.build());
DefaultAudioSink audioSink = new DefaultAudioSink.Builder(context).build();
Format expectedFormat =
new Format.Builder()
.setSampleMimeType(MimeTypes.AUDIO_E_AC3_JOC)
.setChannelCount(2)
.setSampleRate(48_000)
.build();
AudioAttributes expectedAttributes =
new AudioAttributes.Builder()
.setUsage(C.USAGE_MEDIA)
.setContentType(C.AUDIO_CONTENT_TYPE_MUSIC)
.build();
AudioAttributes otherAttributes =
new AudioAttributes.Builder()
.setUsage(C.USAGE_MEDIA)
.setContentType(C.AUDIO_CONTENT_TYPE_MOVIE)
.build();
// Format supported with right attributes.
audioSink.setAudioAttributes(expectedAttributes);
assertThat(audioSink.supportsFormat(expectedFormat)).isTrue();
// Format unsupported with other attributes.
audioSink.setAudioAttributes(otherAttributes);
assertThat(audioSink.supportsFormat(expectedFormat)).isFalse();
}
// Adding the permission to the test AndroidManifest.xml doesn't work to appease lint.
@SuppressWarnings({"StickyBroadcast", "MissingPermission"})
@Test
@Config(minSdk = 23, maxSdk = 32) // AudioManager.TYPE_BLUETOOTH_A2DP is supported from API 23.
public void setPreferredDevice_audioCapabilitiesUpdated() {
// Initially setup the audio sink with Bluetooth and HDMI device connected.
AudioDeviceInfo hdmiDevice =
AudioDeviceInfoBuilder.newBuilder().setType(AudioDeviceInfo.TYPE_HDMI).build();
AudioDeviceInfo bluetoothDevice =
AudioDeviceInfoBuilder.newBuilder().setType(AudioDeviceInfo.TYPE_BLUETOOTH_A2DP).build();
getShadowAudioManager().setOutputDevices(ImmutableList.of(hdmiDevice, bluetoothDevice));
Intent intent = new Intent(AudioManager.ACTION_HDMI_AUDIO_PLUG);
intent.putExtra(AudioManager.EXTRA_AUDIO_PLUG_STATE, 1);
intent.putExtra(
AudioManager.EXTRA_ENCODINGS,
new int[] {MimeTypes.getEncoding(MimeTypes.AUDIO_DTS_HD, /* codec= */ null)});
intent.putExtra(AudioManager.EXTRA_MAX_CHANNEL_COUNT, DEFAULT_MAX_CHANNEL_COUNT);
ApplicationProvider.getApplicationContext().sendStickyBroadcast(intent);
DefaultAudioSink audioSink =
new DefaultAudioSink.Builder(ApplicationProvider.getApplicationContext()).build();
// Verify that surround sound is not supported assuming that Bluetooth is used.
Format surroundFormat =
new Format.Builder()
.setSampleMimeType(MimeTypes.AUDIO_DTS_HD)
.setChannelCount(DEFAULT_MAX_CHANNEL_COUNT)
.build();
assertThat(audioSink.supportsFormat(surroundFormat)).isFalse();
// Set the preferred device to HDMI and assert that the surround sound is now supported.
audioSink.setPreferredDevice(hdmiDevice);
assertThat(audioSink.supportsFormat(surroundFormat)).isTrue();
}
// Adding the permission to the test AndroidManifest.xml doesn't work to appease lint.
@SuppressWarnings({"StickyBroadcast", "MissingPermission"})
@Test
@Config(minSdk = 24, maxSdk = 32) // OnRoutingChangedListener is supported from API 24.
public void onRoutingChanged_onActiveAudioTrack_audioCapabilitiesUpdated() throws Exception {
// Initially setup the audio sink with Bluetooth and HDMI device connected.
AudioDeviceInfo hdmiDevice =
AudioDeviceInfoBuilder.newBuilder().setType(AudioDeviceInfo.TYPE_HDMI).build();
AudioDeviceInfo bluetoothDevice =
AudioDeviceInfoBuilder.newBuilder().setType(AudioDeviceInfo.TYPE_BLUETOOTH_A2DP).build();
getShadowAudioManager().setOutputDevices(ImmutableList.of(hdmiDevice, bluetoothDevice));
Intent intent = new Intent(AudioManager.ACTION_HDMI_AUDIO_PLUG);
intent.putExtra(AudioManager.EXTRA_AUDIO_PLUG_STATE, 1);
intent.putExtra(
AudioManager.EXTRA_ENCODINGS,
new int[] {MimeTypes.getEncoding(MimeTypes.AUDIO_DTS_HD, /* codec= */ null)});
intent.putExtra(AudioManager.EXTRA_MAX_CHANNEL_COUNT, DEFAULT_MAX_CHANNEL_COUNT);
ApplicationProvider.getApplicationContext().sendStickyBroadcast(intent);
DefaultAudioSink audioSink =
new DefaultAudioSink.Builder(ApplicationProvider.getApplicationContext()).build();
// Routing changes are only expected to work on active audio tracks.
Format format =
new Format.Builder()
.setSampleMimeType(MimeTypes.AUDIO_RAW)
.setPcmEncoding(C.ENCODING_PCM_16BIT)
.setChannelCount(2)
.setSampleRate(44100)
.build();
audioSink.configure(format, /* specifiedBufferSize= */ 0, /* outputChannels= */ null);
ByteBuffer silenceBuffer =
ByteBuffer.allocateDirect(/* sample rate * bit depth * channels */ 44100 * 2 * 2)
.order(ByteOrder.nativeOrder());
audioSink.handleBuffer(
silenceBuffer, /* presentationTimeUs= */ 0, /* encodedAccessUnitCount= */ 1);
// Verify that surround sound is not supported assuming that Bluetooth is used.
Format surroundFormat =
new Format.Builder()
.setSampleMimeType(MimeTypes.AUDIO_DTS_HD)
.setChannelCount(DEFAULT_MAX_CHANNEL_COUNT)
.build();
assertThat(audioSink.supportsFormat(surroundFormat)).isFalse();
// Changed the routing to HDMI and assert that the surround sound is now supported.
ShadowAudioTrack.setRoutedDevice(hdmiDevice);
ShadowLooper.idleMainLooper();
assertThat(audioSink.supportsFormat(surroundFormat)).isTrue();
}
@Test
@Config(minSdk = 23) // AudioManager.TYPE_BLUETOOTH_A2DP is supported from API 23.
public void afterRelease_bluetoothDeviceAdded_audioCapabilitiesShouldNotBeUpdated() {
// Set UI mode to TV.
getShadowUiModeManager().currentModeType = Configuration.UI_MODE_TYPE_TELEVISION;
// Initially setup the audio sink with HDMI device connected.
addHdmiDevice();
DefaultAudioSink audioSink =
new DefaultAudioSink.Builder(ApplicationProvider.getApplicationContext()).build();
assertThat(
audioSink.supportsFormat(
new Format.Builder()
.setSampleMimeType(MimeTypes.AUDIO_DTS_HD)
.setChannelCount(DEFAULT_MAX_CHANNEL_COUNT)
.build()))
.isTrue();
audioSink.release();
// Add a bluetooth device after release.
addBluetoothDevice();
// The audio sink should not change its capabilities.
assertThat(
audioSink.supportsFormat(
new Format.Builder()
.setSampleMimeType(MimeTypes.AUDIO_DTS_HD)
.setChannelCount(DEFAULT_MAX_CHANNEL_COUNT)
.build()))
.isTrue();
}
@Test
@Config(minSdk = 21) // AudioManager.ACTION_HDMI_AUDIO_PLUG is supported from API 21.
public void afterRelease_hdmiDeviceAdded_audioCapabilitiesShouldNotBeUpdated() {
// Set UI mode to TV.
getShadowUiModeManager().currentModeType = Configuration.UI_MODE_TYPE_TELEVISION;
// Initially setup the audio sink.
DefaultAudioSink audioSink =
new DefaultAudioSink.Builder(ApplicationProvider.getApplicationContext()).build();
assertThat(
audioSink.supportsFormat(
new Format.Builder()
.setSampleMimeType(MimeTypes.AUDIO_DTS_HD)
.setChannelCount(DEFAULT_MAX_CHANNEL_COUNT)
.build()))
.isFalse();
audioSink.release();
// Add an HDMI device after release.
addHdmiDevice();
// The audio sink should not change its capabilities.
assertThat(
audioSink.supportsFormat(
new Format.Builder()
.setSampleMimeType(MimeTypes.AUDIO_DTS_HD)
.setChannelCount(DEFAULT_MAX_CHANNEL_COUNT)
.build()))
.isFalse();
}
@Test
public void configure_throwsConfigurationException_withInvalidInput() {
Format format = new Format.Builder().setSampleMimeType(MimeTypes.AUDIO_AAC).build();
@ -414,6 +771,145 @@ public final class DefaultAudioSinkTest {
defaultAudioSink.configure(format, /* specifiedBufferSize= */ 0, /* outputChannels= */ null);
}
// Adding the permission to the test AndroidManifest.xml doesn't work to appease lint.
@SuppressWarnings({"StickyBroadcast", "MissingPermission"})
private void addHdmiDevice() {
if (Util.SDK_INT >= 23) {
// AudioFormat.getChannelIndexMask() in the implementation of
// ShadowAudioTrack.addDirectPlaybackSupport requires API 23+.
// https://cs.android.com/android/platform/superproject/main/+/main:external/robolectric/shadows/framework/src/main/java/org/robolectric/shadows/ShadowAudioTrack.java?q=format.getChannelIndexMask()
ShadowAudioTrack.addAllowedNonPcmEncoding(AudioFormat.ENCODING_DTS_HD);
ShadowAudioTrack.addDirectPlaybackSupport(
new AudioFormat.Builder()
.setEncoding(AudioFormat.ENCODING_DTS_HD)
.setSampleRate(AudioCapabilities.DEFAULT_SAMPLE_RATE_HZ)
.setChannelMask(AudioFormat.CHANNEL_OUT_STEREO)
.build(),
new android.media.AudioAttributes.Builder()
.setUsage(android.media.AudioAttributes.USAGE_MEDIA)
.setContentType(android.media.AudioAttributes.CONTENT_TYPE_UNKNOWN)
.setFlags(0)
.build());
// AudioDeviceInfoBuilder requires API 23+.
// https://cs.android.com/android/platform/superproject/main/+/main:external/robolectric/shadows/framework/src/main/java/org/robolectric/shadows/AudioDeviceInfoBuilder.java?q=VERSION_CODES.M
AudioDeviceInfoBuilder hdmiDeviceBuilder =
AudioDeviceInfoBuilder.newBuilder().setType(AudioDeviceInfo.TYPE_HDMI);
if (Util.SDK_INT >= 33) {
ImmutableList<AudioProfile> expectedProfiles =
ImmutableList.of(
AudioProfileBuilder.newBuilder()
.setFormat(AudioFormat.ENCODING_DTS_HD)
.setSamplingRates(new int[] {48_000})
.setChannelMasks(
new int[] {Util.getAudioTrackChannelConfig(DEFAULT_MAX_CHANNEL_COUNT)})
.setChannelIndexMasks(new int[] {})
.setEncapsulationType(AudioProfile.AUDIO_ENCAPSULATION_TYPE_NONE)
.build(),
AudioProfileBuilder.newBuilder()
.setFormat(AudioFormat.ENCODING_PCM_16BIT)
.setSamplingRates(new int[] {48_000})
.setChannelMasks(new int[] {AudioFormat.CHANNEL_OUT_STEREO})
.setChannelIndexMasks(new int[] {})
.setEncapsulationType(AudioProfile.AUDIO_ENCAPSULATION_TYPE_NONE)
.build());
hdmiDeviceBuilder.setProfiles(expectedProfiles);
}
hdmiDevice = hdmiDeviceBuilder.build();
getShadowAudioManager()
.addOutputDevice(checkNotNull(hdmiDevice), /* notifyAudioDeviceCallbacks= */ true);
getShadowAudioManager().addOutputDeviceWithDirectProfiles(checkNotNull(hdmiDevice));
}
Intent intent = new Intent(AudioManager.ACTION_HDMI_AUDIO_PLUG);
intent.putExtra(AudioManager.EXTRA_AUDIO_PLUG_STATE, 1);
intent.putExtra(
AudioManager.EXTRA_ENCODINGS,
new int[] {MimeTypes.getEncoding(MimeTypes.AUDIO_DTS_HD, /* codec= */ null)});
intent.putExtra(AudioManager.EXTRA_MAX_CHANNEL_COUNT, DEFAULT_MAX_CHANNEL_COUNT);
ApplicationProvider.getApplicationContext().sendStickyBroadcast(intent);
shadowOf(Looper.getMainLooper()).idle();
}
// Adding the permission to the test AndroidManifest.xml doesn't work to appease lint.
@SuppressWarnings({"StickyBroadcast", "MissingPermission"})
private void removeHdmiDevice() {
if (Util.SDK_INT >= 23 && hdmiDevice != null) {
ShadowAudioTrack.clearAllowedNonPcmEncodings();
ShadowAudioTrack.clearDirectPlaybackSupportedFormats();
getShadowAudioManager().removeOutputDeviceWithDirectProfiles(hdmiDevice);
getShadowAudioManager()
.removeOutputDevice(checkNotNull(hdmiDevice), /* notifyAudioDeviceCallbacks= */ true);
getShadowAudioManager().removeOutputDeviceWithDirectProfiles(hdmiDevice);
hdmiDevice = null;
}
Intent intent = new Intent(AudioManager.ACTION_HDMI_AUDIO_PLUG);
intent.putExtra(AudioManager.EXTRA_AUDIO_PLUG_STATE, 0);
ApplicationProvider.getApplicationContext().sendStickyBroadcast(intent);
shadowOf(Looper.getMainLooper()).idle();
}
private void addBluetoothDevice() {
if (Util.SDK_INT >= 23) {
// For API 33+, AudioManager.getDirectProfilesForAttributes returns the AudioProfile for the
// routed device. To simulate the Bluetooth is connected and routed, we need to remove the
// profile of the HDMI device, which means that the HDMI device is no longer routed, but
// still be connected.
removeHdmiDevice();
AudioDeviceInfoBuilder bluetoothDeviceBuilder =
AudioDeviceInfoBuilder.newBuilder().setType(AudioDeviceInfo.TYPE_BLUETOOTH_A2DP);
if (Util.SDK_INT >= 33) {
bluetoothDeviceBuilder.setProfiles(ImmutableList.of(createPcmProfile()));
}
bluetoothDevice = bluetoothDeviceBuilder.build();
getShadowAudioManager()
.addOutputDevice(checkNotNull(bluetoothDevice), /* notifyAudioDeviceCallbacks= */ true);
getShadowAudioManager().addOutputDeviceWithDirectProfiles(checkNotNull(bluetoothDevice));
}
shadowOf(Looper.getMainLooper()).idle();
}
private void removeBluetoothDevice() {
if (Util.SDK_INT >= 23 && bluetoothDevice != null) {
// Add back the HDMI device back as the routed device to simulate that the bluetooth device
// has gone and is no longer routed.
addHdmiDevice();
getShadowAudioManager().removeOutputDeviceWithDirectProfiles(checkNotNull(bluetoothDevice));
getShadowAudioManager()
.removeOutputDevice(
checkNotNull(bluetoothDevice), /* notifyAudioDeviceCallbacks= */ true);
bluetoothDevice = null;
}
shadowOf(Looper.getMainLooper()).idle();
}
private static ShadowUIModeManager getShadowUiModeManager() {
return shadowOf(
(UiModeManager)
ApplicationProvider.getApplicationContext().getSystemService(Context.UI_MODE_SERVICE));
}
private static ShadowAudioManager getShadowAudioManager() {
return shadowOf(
(AudioManager)
ApplicationProvider.getApplicationContext().getSystemService(Context.AUDIO_SERVICE));
}
@SuppressWarnings("UseSdkSuppress") // https://issuetracker.google.com/382253664
@RequiresApi(33)
private static AudioProfile createPcmProfile() {
return AudioProfileBuilder.newBuilder()
.setFormat(AudioFormat.ENCODING_PCM_16BIT)
.setSamplingRates(new int[] {48_000})
.setChannelMasks(new int[] {AudioFormat.CHANNEL_OUT_STEREO})
.setChannelIndexMasks(new int[] {})
.setEncapsulationType(AudioProfile.AUDIO_ENCAPSULATION_TYPE_NONE)
.build();
}
/** Creates a one second silence buffer for 44.1 kHz stereo 16-bit audio. */
private static ByteBuffer create1Sec44100HzSilenceBuffer() {
return ByteBuffer.allocateDirect(