Selectable builtin speaker support for Wear OS

The builtin speaker is to be supported as a suitable output when that is deliberately selected for the media playback by the user in Wear OS.

PiperOrigin-RevId: 655950824
This commit is contained in:
Googler 2024-07-25 07:40:35 -07:00 committed by Copybara-Service
parent 685ea1e616
commit 300453820c
7 changed files with 289 additions and 5 deletions

View File

@ -26,6 +26,8 @@
timeout ([#1540](https://github.com/androidx/media/issues/1540)).
* Remove `MediaCodecAdapter.Configuration.flags` as the field was always
zero.
* Allow the user to select the built-in speaker for playback on Wear OS
API 35+ (where the device advertises support for this).
* Transformer:
* Add `SurfaceAssetLoader`, which supports queueing video data to
Transformer via a `Surface`.

View File

@ -0,0 +1,95 @@
/*
* Copyright (C) 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;
import static androidx.media3.common.util.Assertions.checkState;
import android.content.Context;
import android.media.MediaRoute2Info;
import android.media.MediaRouter2;
import android.media.MediaRouter2.RouteCallback;
import android.media.RouteDiscoveryPreference;
import android.media.RoutingSessionInfo;
import android.os.Handler;
import androidx.annotation.RequiresApi;
import androidx.media3.common.util.Util;
import com.google.common.collect.ImmutableList;
import java.util.concurrent.Executor;
/** Default implementation for {@link SuitableOutputChecker}. */
@RequiresApi(35)
/* package */ final class DefaultSuitableOutputChecker implements SuitableOutputChecker {
private static final RouteDiscoveryPreference EMPTY_DISCOVERY_PREFERENCE =
new RouteDiscoveryPreference.Builder(
/* preferredFeatures= */ ImmutableList.of(), /* activeScan= */ false)
.build();
private final MediaRouter2 router;
private final RouteCallback routeCallback;
private final Executor executor;
private boolean isEnabled;
public DefaultSuitableOutputChecker(Context context, Handler eventHandler) {
this.router = MediaRouter2.getInstance(context);
this.routeCallback = new RouteCallback() {};
this.executor =
new Executor() {
@Override
public void execute(Runnable command) {
Util.postOrRun(eventHandler, command);
}
};
}
@Override
public void setEnabled(boolean isEnabled) {
if (isEnabled && !this.isEnabled) {
router.registerRouteCallback(executor, routeCallback, EMPTY_DISCOVERY_PREFERENCE);
this.isEnabled = true;
} else if (!isEnabled && this.isEnabled) {
router.unregisterRouteCallback(routeCallback);
this.isEnabled = false;
}
}
@Override
public boolean isSelectedRouteSuitableForPlayback() {
checkState(isEnabled, "SuitableOutputChecker is not enabled");
int transferReason = router.getSystemController().getRoutingSessionInfo().getTransferReason();
boolean wasTransferInitiatedBySelf = router.getSystemController().wasTransferInitiatedBySelf();
for (MediaRoute2Info routeInfo : router.getSystemController().getSelectedRoutes()) {
if (isRouteSuitableForMediaPlayback(routeInfo, transferReason, wasTransferInitiatedBySelf)) {
return true;
}
}
return false;
}
private static boolean isRouteSuitableForMediaPlayback(
MediaRoute2Info routeInfo, int transferReason, boolean wasTransferInitiatedBySelf) {
int suitabilityStatus = routeInfo.getSuitabilityStatus();
if (suitabilityStatus == MediaRoute2Info.SUITABILITY_STATUS_SUITABLE_FOR_MANUAL_TRANSFER) {
return (transferReason == RoutingSessionInfo.TRANSFER_REASON_SYSTEM_REQUEST
|| transferReason == RoutingSessionInfo.TRANSFER_REASON_APP)
&& wasTransferInitiatedBySelf;
}
return suitabilityStatus == MediaRoute2Info.SUITABILITY_STATUS_SUITABLE_FOR_DEFAULT_TRANSFER;
}
}

View File

@ -15,6 +15,7 @@
*/
package androidx.media3.exoplayer;
import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
import static androidx.media3.common.util.Assertions.checkArgument;
import static androidx.media3.common.util.Assertions.checkNotNull;
import static androidx.media3.common.util.Assertions.checkState;
@ -23,6 +24,7 @@ import android.content.Context;
import android.media.AudioDeviceInfo;
import android.media.AudioTrack;
import android.media.MediaCodec;
import android.os.Handler;
import android.os.Looper;
import android.os.Process;
import android.view.Surface;
@ -32,6 +34,7 @@ import android.view.TextureView;
import androidx.annotation.IntRange;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.annotation.RestrictTo;
import androidx.annotation.VisibleForTesting;
import androidx.media3.common.AudioAttributes;
import androidx.media3.common.AuxEffectInfo;
@ -506,6 +509,7 @@ public interface ExoPlayer extends Player {
/* package */ boolean suppressPlaybackOnUnsuitableOutput;
/* package */ String playerName;
/* package */ boolean dynamicSchedulingEnabled;
@Nullable /* package */ SuitableOutputChecker suitableOutputChecker;
/**
* Creates a builder.
@ -1260,6 +1264,27 @@ public interface ExoPlayer extends Player {
return this;
}
/**
* Sets the {@link SuitableOutputChecker} to check the suitability of the selected outputs for
* playback.
*
* <p>If this method is not called, the library uses a default implementation based on framework
* APIs.
*
* @return This builder.
* @throws IllegalStateException If {@link #build()} has already been called.
*/
@CanIgnoreReturnValue
@UnstableApi
@RestrictTo(LIBRARY_GROUP)
@VisibleForTesting
@RequiresApi(35)
public Builder setSuitableOutputChecker(SuitableOutputChecker suitableOutputChecker) {
checkState(!buildCalled);
this.suitableOutputChecker = suitableOutputChecker;
return this;
}
/**
* Sets the {@link Looper} that will be used for playback.
*
@ -1304,6 +1329,11 @@ public interface ExoPlayer extends Player {
public ExoPlayer build() {
checkState(!buildCalled);
buildCalled = true;
if (suitableOutputChecker == null
&& Util.SDK_INT >= 35
&& suppressPlaybackOnUnsuitableOutput) {
suitableOutputChecker = new DefaultSuitableOutputChecker(context, new Handler(looper));
}
return new ExoPlayerImpl(/* builder= */ this, /* wrappingPlayer= */ null);
}

View File

@ -184,6 +184,7 @@ import java.util.concurrent.TimeoutException;
private final long detachSurfaceTimeoutMs;
@Nullable private AudioManager audioManager;
private final boolean suppressPlaybackOnUnsuitableOutput;
@Nullable private final SuitableOutputChecker suitableOutputChecker;
private @RepeatMode int repeatMode;
private boolean shuffleModeEnabled;
@ -400,13 +401,18 @@ import java.util.concurrent.TimeoutException;
audioBecomingNoisyManager.setEnabled(builder.handleAudioBecomingNoisy);
audioFocusManager = new AudioFocusManager(builder.context, eventHandler, componentListener);
audioFocusManager.setAudioAttributes(builder.handleAudioFocus ? audioAttributes : null);
if (suppressPlaybackOnUnsuitableOutput && Util.SDK_INT >= 23) {
suitableOutputChecker = builder.suitableOutputChecker;
if (suitableOutputChecker != null && Util.SDK_INT >= 35) {
suitableOutputChecker.setEnabled(true);
} else if (suppressPlaybackOnUnsuitableOutput && Util.SDK_INT >= 23) {
audioManager = (AudioManager) applicationContext.getSystemService(Context.AUDIO_SERVICE);
Api23.registerAudioDeviceCallback(
audioManager,
new NoSuitableOutputPlaybackSuppressionAudioDeviceCallback(),
new Handler(applicationLooper));
}
if (builder.deviceVolumeControlEnabled) {
streamVolumeManager =
new StreamVolumeManager(builder.context, eventHandler, componentListener);
@ -1066,6 +1072,9 @@ import java.util.concurrent.TimeoutException;
if (playbackInfo.sleepingForOffload) {
playbackInfo = playbackInfo.copyWithEstimatedPosition();
}
if (suitableOutputChecker != null && Util.SDK_INT >= 35) {
suitableOutputChecker.setEnabled(false);
}
playbackInfo = playbackInfo.copyWithPlaybackState(Player.STATE_IDLE);
playbackInfo = playbackInfo.copyWithLoadingMediaPeriodId(playbackInfo.periodId);
playbackInfo.bufferedPositionUs = playbackInfo.positionUs;
@ -2831,13 +2840,16 @@ import java.util.concurrent.TimeoutException;
}
private boolean hasSupportedAudioOutput() {
if (audioManager == null || Util.SDK_INT < 23) {
if (Util.SDK_INT >= 35 && suitableOutputChecker != null) {
return suitableOutputChecker.isSelectedRouteSuitableForPlayback();
} else if (Util.SDK_INT >= 23 && audioManager != null) {
return Api23.isSuitableExternalAudioOutputPresentInAudioDeviceInfoList(
applicationContext, audioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS));
} else {
// The Audio Manager API to determine the list of connected audio devices is available only in
// API >= 23.
return true;
}
return Api23.isSuitableAudioOutputPresentInAudioDeviceInfoList(
applicationContext, audioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS));
}
private void updateWakeAndWifiLock() {
@ -3390,7 +3402,7 @@ import java.util.concurrent.TimeoutException;
private Api23() {}
@DoNotInline
public static boolean isSuitableAudioOutputPresentInAudioDeviceInfoList(
public static boolean isSuitableExternalAudioOutputPresentInAudioDeviceInfoList(
Context context, AudioDeviceInfo[] audioDeviceInfos) {
if (!Util.isWear(context)) {
return true;

View File

@ -0,0 +1,46 @@
/*
* Copyright (C) 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;
import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
import androidx.annotation.RequiresApi;
import androidx.annotation.RestrictTo;
import androidx.media3.common.util.UnstableApi;
/** Provides methods to check the suitability of media outputs. */
@RequiresApi(35)
@RestrictTo(LIBRARY_GROUP)
@UnstableApi
public interface SuitableOutputChecker {
/**
* Enables the current instance to receive updates on the suitable media outputs.
*
* <p>When the caller no longer requires updated information, they must call this method with
* {@code false}.
*
* @param isEnabled True if this instance should receive the updates.
*/
void setEnabled(boolean isEnabled);
/**
* Returns whether any audio output is suitable for the media playback.
*
* @throws IllegalStateException if this instance is not enabled to receive the updates on
* suitable media outputs.
*/
boolean isSelectedRouteSuitableForPlayback();
}

View File

@ -0,0 +1,75 @@
/*
* Copyright (C) 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.test.utils;
import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP;
import static androidx.media3.common.util.Assertions.checkState;
import androidx.annotation.RequiresApi;
import androidx.annotation.RestrictTo;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.exoplayer.SuitableOutputChecker;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
/** Fake implementation for {@link SuitableOutputChecker}. */
@RestrictTo(LIBRARY_GROUP)
@UnstableApi
@RequiresApi(35)
public final class FakeSuitableOutputChecker implements SuitableOutputChecker {
/** Builder for {@link FakeSuitableOutputChecker} instance. */
public static final class Builder {
private boolean isSuitableOutputAvailable;
/**
* Sets the initial value to be returned from {@link
* SuitableOutputChecker#isSelectedRouteSuitableForPlayback()}. The default value is false.
*/
@CanIgnoreReturnValue
public Builder setIsSuitableExternalOutputAvailable(boolean isSuitableOutputAvailable) {
this.isSuitableOutputAvailable = isSuitableOutputAvailable;
return this;
}
/**
* Builds a {@link FakeSuitableOutputChecker} with the builder's current values.
*
* @return The built {@link FakeSuitableOutputChecker}.
*/
public FakeSuitableOutputChecker build() {
return new FakeSuitableOutputChecker(isSuitableOutputAvailable);
}
}
private final boolean isSuitableOutputAvailable;
private boolean isEnabled;
public FakeSuitableOutputChecker(boolean isSuitableOutputAvailable) {
this.isSuitableOutputAvailable = isSuitableOutputAvailable;
}
@Override
public void setEnabled(boolean isEnabled) {
this.isEnabled = isEnabled;
}
@Override
public boolean isSelectedRouteSuitableForPlayback() {
checkState(isEnabled, "SuitableOutputChecker is not enabled");
return isSuitableOutputAvailable;
}
}

View File

@ -18,8 +18,10 @@ package androidx.media3.test.utils;
import static com.google.common.truth.Truth.assertThat;
import android.content.Context;
import android.os.Build.VERSION;
import android.os.Looper;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.media3.common.C;
import androidx.media3.common.util.Assertions;
import androidx.media3.common.util.Clock;
@ -30,6 +32,7 @@ import androidx.media3.exoplayer.ExoPlayer;
import androidx.media3.exoplayer.LoadControl;
import androidx.media3.exoplayer.Renderer;
import androidx.media3.exoplayer.RenderersFactory;
import androidx.media3.exoplayer.SuitableOutputChecker;
import androidx.media3.exoplayer.analytics.DefaultAnalyticsCollector;
import androidx.media3.exoplayer.source.MediaSource;
import androidx.media3.exoplayer.trackselection.DefaultTrackSelector;
@ -52,6 +55,7 @@ public class TestExoPlayerBuilder {
@Nullable private MediaSource.Factory mediaSourceFactory;
private boolean useLazyPreparation;
private @MonotonicNonNull Looper looper;
@Nullable private SuitableOutputChecker suitableOutputChecker;
private long seekBackIncrementMs;
private long seekForwardIncrementMs;
private long maxSeekToPreviousPositionMs;
@ -242,6 +246,23 @@ public class TestExoPlayerBuilder {
return this;
}
/**
* Sets the {@link SuitableOutputChecker} to check the suitability of the selected outputs for
* playback.
*
* <p>If this method is not called, the library uses a default implementation based on framework
* APIs.
*
* @return This builder.
*/
@CanIgnoreReturnValue
@RequiresApi(35)
public TestExoPlayerBuilder setSuitableOutputChecker(
SuitableOutputChecker suitableOutputChecker) {
this.suitableOutputChecker = suitableOutputChecker;
return this;
}
/**
* Returns the {@link Looper} that will be used by the player, or null if no {@link Looper} has
* been set yet and no default is available.
@ -402,6 +423,9 @@ public class TestExoPlayerBuilder {
.setDeviceVolumeControlEnabled(deviceVolumeControlEnabled)
.setSuppressPlaybackOnUnsuitableOutput(suppressPlaybackWhenUnsuitableOutput)
.experimentalSetDynamicSchedulingEnabled(dynamicSchedulingEnabled);
if (VERSION.SDK_INT >= 35 && suitableOutputChecker != null) {
builder.setSuitableOutputChecker(suitableOutputChecker);
}
if (mediaSourceFactory != null) {
builder.setMediaSourceFactory(mediaSourceFactory);
}