mirror of
https://github.com/androidx/media.git
synced 2025-04-30 06:46:50 +08:00
Publish some internal tests now we depend on Robolectric 4.14.1
PiperOrigin-RevId: 705411400
This commit is contained in:
parent
f587ac2a67
commit
0b0c198f59
@ -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");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
@ -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 androidx.media3.exoplayer.audio.AudioSink.SINK_FORMAT_SUPPORTED_WITH_TRANSCODING;
|
||||||
import static com.google.common.truth.Truth.assertThat;
|
import static com.google.common.truth.Truth.assertThat;
|
||||||
import static org.junit.Assert.assertThrows;
|
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.C;
|
||||||
import androidx.media3.common.Format;
|
import androidx.media3.common.Format;
|
||||||
import androidx.media3.common.MimeTypes;
|
import androidx.media3.common.MimeTypes;
|
||||||
import androidx.media3.common.PlaybackParameters;
|
import androidx.media3.common.PlaybackParameters;
|
||||||
import androidx.media3.common.audio.AudioProcessor;
|
import androidx.media3.common.audio.AudioProcessor;
|
||||||
import androidx.media3.common.audio.AudioProcessorChain;
|
import androidx.media3.common.audio.AudioProcessorChain;
|
||||||
|
import androidx.media3.common.util.Util;
|
||||||
import androidx.media3.exoplayer.audio.DefaultAudioSink.DefaultAudioProcessorChain;
|
import androidx.media3.exoplayer.audio.DefaultAudioSink.DefaultAudioProcessorChain;
|
||||||
import androidx.test.core.app.ApplicationProvider;
|
import androidx.test.core.app.ApplicationProvider;
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||||
|
import com.google.common.collect.ImmutableList;
|
||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
import java.nio.ByteOrder;
|
import java.nio.ByteOrder;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
import java.util.concurrent.TimeoutException;
|
import java.util.concurrent.TimeoutException;
|
||||||
|
import org.junit.After;
|
||||||
import org.junit.Assert;
|
import org.junit.Assert;
|
||||||
import org.junit.Before;
|
import org.junit.Before;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.junit.runner.RunWith;
|
import org.junit.runner.RunWith;
|
||||||
import org.robolectric.annotation.Config;
|
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.ShadowSystemClock;
|
||||||
|
import org.robolectric.shadows.ShadowUIModeManager;
|
||||||
|
|
||||||
/** Unit tests for {@link DefaultAudioSink}. */
|
/** Unit tests for {@link DefaultAudioSink}. */
|
||||||
@RunWith(AndroidJUnit4.class)
|
@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_MONO = 1;
|
||||||
private static final int CHANNEL_COUNT_STEREO = 2;
|
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 BYTES_PER_FRAME_16_BIT = 2;
|
||||||
private static final int SAMPLE_RATE_44_1 = 44100;
|
private static final int SAMPLE_RATE_44_1 = 44100;
|
||||||
private static final int TRIM_100_MS_FRAME_COUNT = 4410;
|
private static final int TRIM_100_MS_FRAME_COUNT = 4410;
|
||||||
@ -63,6 +86,9 @@ public final class DefaultAudioSinkTest {
|
|||||||
private DefaultAudioSink defaultAudioSink;
|
private DefaultAudioSink defaultAudioSink;
|
||||||
private ArrayAudioBufferSink arrayAudioBufferSink;
|
private ArrayAudioBufferSink arrayAudioBufferSink;
|
||||||
|
|
||||||
|
@Nullable private AudioDeviceInfo hdmiDevice;
|
||||||
|
@Nullable private AudioDeviceInfo bluetoothDevice;
|
||||||
|
|
||||||
@Before
|
@Before
|
||||||
public void setUp() {
|
public void setUp() {
|
||||||
// For capturing output.
|
// For capturing output.
|
||||||
@ -74,6 +100,12 @@ public final class DefaultAudioSinkTest {
|
|||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@After
|
||||||
|
public void tearDown() {
|
||||||
|
removeBluetoothDevice();
|
||||||
|
removeHdmiDevice();
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void handlesSpecializedAudioProcessorArray() {
|
public void handlesSpecializedAudioProcessorArray() {
|
||||||
defaultAudioSink =
|
defaultAudioSink =
|
||||||
@ -359,6 +391,331 @@ public final class DefaultAudioSinkTest {
|
|||||||
assertThat(defaultAudioSink.supportsFormat(aacLcFormat)).isFalse();
|
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
|
@Test
|
||||||
public void configure_throwsConfigurationException_withInvalidInput() {
|
public void configure_throwsConfigurationException_withInvalidInput() {
|
||||||
Format format = new Format.Builder().setSampleMimeType(MimeTypes.AUDIO_AAC).build();
|
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);
|
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. */
|
/** Creates a one second silence buffer for 44.1 kHz stereo 16-bit audio. */
|
||||||
private static ByteBuffer create1Sec44100HzSilenceBuffer() {
|
private static ByteBuffer create1Sec44100HzSilenceBuffer() {
|
||||||
return ByteBuffer.allocateDirect(
|
return ByteBuffer.allocateDirect(
|
||||||
|
Loading…
x
Reference in New Issue
Block a user