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:
parent
685ea1e616
commit
300453820c
@ -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`.
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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();
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user