Compare commits

..

7 Commits

Author SHA1 Message Date
aquilescanta
1ea69ca7be Add @InlineMe to CastPlayer deprecated methods
PiperOrigin-RevId: 747868138
2025-04-15 07:38:16 -07:00
aquilescanta
405365c228 Implement device volume adjustment in CastPlayer
Issue: androidx/media#2089
PiperOrigin-RevId: 747861812
2025-04-15 07:17:41 -07:00
tonihei
70e7121a51 Make some audio processors public
ChannelMappingAudioProcessor, TrimmingAudioProcessor and
ToFloatPcmAudioProcessor are currently package-private even
though they might be useful in custom audio processing chains
or custom version of audio sinks.

Issue: androidx/media#2339
PiperOrigin-RevId: 747857815
2025-04-15 07:03:04 -07:00
tonihei
9cc7dd0dbe Ensure DefaultAudioSink doesn't store non-application Context
The Context is currently passed right down from ExoPlayer.Builder
without ever converting it to the application context. This may
cause memory leaks if a Player is kept across activities/service
lifecycles.

PiperOrigin-RevId: 747835487
2025-04-15 05:40:56 -07:00
tonihei
21514ba8e8 Add missing check for TRACK_TYPE_NONE before accessing selections
The track selections will be null if the track type is NONE even
if the renderer is enabled.

PiperOrigin-RevId: 747818128
2025-04-15 04:38:14 -07:00
bachinger
117db48907 Assert preload looper is not the main looper
Issue: androidx/media#2315
PiperOrigin-RevId: 747809915
2025-04-15 04:08:01 -07:00
dancho
9fca713045 Ignore flaky test
PiperOrigin-RevId: 747793474
2025-04-15 03:06:51 -07:00
17 changed files with 313 additions and 72 deletions

View File

@ -23,6 +23,9 @@
* DataSource:
* Audio:
* Allow constant power upmixing/downmixing in DefaultAudioMixer.
* Make `ChannelMappingAudioProcessor`, `TrimmingAudioProcessor` and
`ToFloatPcmAudioProcessor` public
([#2339](https://github.com/androidx/media/issues/2339)).
* Video:
* Add experimental `ExoPlayer` API to include the
`MediaCodec.BUFFER_FLAG_DECODE_ONLY` flag when queuing decode-only input
@ -68,6 +71,9 @@
* MIDI extension:
* Leanback extension:
* Cast extension:
* Add support for `getDeviceVolume()`, `setDeviceVolume()`,
`getDeviceMuted()`, and `setDeviceMuted()`
([#2089](https://github.com/androidx/media/issues/2089)).
* Test Utilities:
* Removed `transformer.TestUtil.addAudioDecoders(String...)`,
`transformer.TestUtil.addAudioEncoders(String...)`, and

View File

@ -27,6 +27,7 @@ dependencies {
api 'com.google.android.gms:play-services-cast-framework:21.5.0'
implementation 'androidx.annotation:annotation:' + androidxAnnotationVersion
api project(modulePrefix + 'lib-common')
compileOnly 'com.google.errorprone:error_prone_annotations:' + errorProneVersion
compileOnly 'org.checkerframework:checker-qual:' + checkerframeworkVersion
compileOnly 'org.jetbrains.kotlin:kotlin-annotations-jvm:' + kotlinAnnotationsVersion
testImplementation project(modulePrefix + 'test-utils')

View File

@ -29,6 +29,7 @@ import android.media.MediaRouter2.TransferCallback;
import android.media.RouteDiscoveryPreference;
import android.os.Handler;
import android.os.Looper;
import android.util.Range;
import android.view.Surface;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
@ -59,6 +60,7 @@ import androidx.media3.common.util.Log;
import androidx.media3.common.util.Size;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.common.util.Util;
import com.google.android.gms.cast.Cast;
import com.google.android.gms.cast.CastStatusCodes;
import com.google.android.gms.cast.MediaInfo;
import com.google.android.gms.cast.MediaQueueItem;
@ -73,6 +75,8 @@ import com.google.android.gms.cast.framework.media.RemoteMediaClient.MediaChanne
import com.google.android.gms.common.api.PendingResult;
import com.google.android.gms.common.api.ResultCallback;
import com.google.common.collect.ImmutableList;
import com.google.errorprone.annotations.InlineMe;
import java.io.IOException;
import java.util.List;
import java.util.Objects;
import org.checkerframework.checker.nullness.qual.RequiresNonNull;
@ -93,12 +97,23 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
@UnstableApi
public final class CastPlayer extends BasePlayer {
/**
* Maximum volume to use for {@link #getDeviceVolume()} and {@link #setDeviceVolume}.
*
* <p>These methods are implemented around {@link CastSession#setVolume} and {@link
* CastSession#getVolume} which operate on a {@code [0, 1]} range. So this value allows us to
* convert to and from the int-based volume scale that {@link #getDeviceVolume()} uses.
*/
private static final int MAX_VOLUME = 20;
/**
* A {@link DeviceInfo#PLAYBACK_TYPE_REMOTE remote} {@link DeviceInfo} with a null {@link
* DeviceInfo#routingControllerId}.
*/
public static final DeviceInfo DEVICE_INFO_REMOTE_EMPTY =
new DeviceInfo.Builder(DeviceInfo.PLAYBACK_TYPE_REMOTE).build();
new DeviceInfo.Builder(DeviceInfo.PLAYBACK_TYPE_REMOTE).setMaxVolume(MAX_VOLUME).build();
private static final Range<Integer> VOLUME_RANGE = new Range<>(0, MAX_VOLUME);
static {
MediaLibraryInfo.registerModule("media3.cast");
@ -113,6 +128,11 @@ public final class CastPlayer extends BasePlayer {
COMMAND_STOP,
COMMAND_SEEK_TO_DEFAULT_POSITION,
COMMAND_SEEK_TO_MEDIA_ITEM,
COMMAND_GET_DEVICE_VOLUME,
COMMAND_ADJUST_DEVICE_VOLUME,
COMMAND_ADJUST_DEVICE_VOLUME_WITH_FLAGS,
COMMAND_SET_DEVICE_VOLUME,
COMMAND_SET_DEVICE_VOLUME_WITH_FLAGS,
COMMAND_SET_REPEAT_MODE,
COMMAND_SET_SPEED_AND_PITCH,
COMMAND_GET_CURRENT_MEDIA_ITEM,
@ -144,6 +164,8 @@ public final class CastPlayer extends BasePlayer {
@Nullable private final Api30Impl api30Impl;
// Result callbacks.
private final Cast.Listener castListener;
private final StatusListener statusListener;
private final SeekResultCallback seekResultCallback;
@ -154,7 +176,10 @@ public final class CastPlayer extends BasePlayer {
// Internal state.
private final StateHolder<Boolean> playWhenReady;
private final StateHolder<Integer> repeatMode;
private boolean isMuted;
private int deviceVolume;
private final StateHolder<PlaybackParameters> playbackParameters;
@Nullable private CastSession castSession;
@Nullable private RemoteMediaClient remoteMediaClient;
private CastTimeline currentTimeline;
private Tracks currentTracks;
@ -257,6 +282,7 @@ public final class CastPlayer extends BasePlayer {
this.maxSeekToPreviousPositionMs = maxSeekToPreviousPositionMs;
timelineTracker = new CastTimelineTracker(mediaItemConverter);
period = new Timeline.Period();
castListener = new CastListener();
statusListener = new StatusListener();
seekResultCallback = new SeekResultCallback();
listeners =
@ -266,6 +292,7 @@ public final class CastPlayer extends BasePlayer {
(listener, flags) -> listener.onEvents(/* player= */ this, new Events(flags)));
playWhenReady = new StateHolder<>(false);
repeatMode = new StateHolder<>(REPEAT_MODE_OFF);
deviceVolume = MAX_VOLUME;
playbackParameters = new StateHolder<>(PlaybackParameters.DEFAULT);
playbackState = STATE_IDLE;
currentTimeline = CastTimeline.EMPTY_CAST_TIMELINE;
@ -278,8 +305,7 @@ public final class CastPlayer extends BasePlayer {
SessionManager sessionManager = castContext.getSessionManager();
sessionManager.addSessionManagerListener(statusListener, CastSession.class);
CastSession session = sessionManager.getCurrentCastSession();
setRemoteMediaClient(session != null ? session.getRemoteMediaClient() : null);
setCastSession(sessionManager.getCurrentCastSession());
updateInternalStateAndNotifyIfChanged();
if (SDK_INT >= 30 && context != null) {
api30Impl = new Api30Impl(context);
@ -829,61 +855,98 @@ public final class CastPlayer extends BasePlayer {
return deviceInfo;
}
/** This method is not supported and always returns {@code 0}. */
@Override
public int getDeviceVolume() {
return 0;
return deviceVolume;
}
/** This method is not supported and always returns {@code false}. */
@Override
public boolean isDeviceMuted() {
return false;
return isMuted;
}
/**
* @deprecated Use {@link #setDeviceVolume(int, int)} instead.
*/
@InlineMe(replacement = "this.setDeviceVolume(volume, 0)")
@Deprecated
@Override
public void setDeviceVolume(int volume) {}
public void setDeviceVolume(@IntRange(from = 0) int volume) {
setDeviceVolume(volume, /* flags= */ 0);
}
/** This method is not supported and does nothing. */
@Override
public void setDeviceVolume(int volume, @C.VolumeFlags int flags) {}
public void setDeviceVolume(@IntRange(from = 0) int volume, @C.VolumeFlags int flags) {
if (castSession == null) {
return;
}
volume = VOLUME_RANGE.clamp(volume);
try {
// See [Internal ref: b/399691860] for context on why we don't use
// RemoteMediaClient.setStreamVolume.
castSession.setVolume((float) volume / MAX_VOLUME);
} catch (IOException e) {
Log.w(TAG, "Ignoring setDeviceVolume due to exception", e);
return;
}
setDeviceVolumeAndNotifyIfChanged(volume, isMuted);
listeners.flushEvents();
}
/**
* @deprecated Use {@link #increaseDeviceVolume(int)} instead.
*/
@InlineMe(replacement = "this.increaseDeviceVolume(0)")
@Deprecated
@Override
public void increaseDeviceVolume() {}
public void increaseDeviceVolume() {
increaseDeviceVolume(/* flags= */ 0);
}
/** This method is not supported and does nothing. */
@Override
public void increaseDeviceVolume(@C.VolumeFlags int flags) {}
public void increaseDeviceVolume(@C.VolumeFlags int flags) {
setDeviceVolume(getDeviceVolume() + 1, flags);
}
/**
* @deprecated Use {@link #decreaseDeviceVolume(int)} instead.
* @deprecated Use {@link #decreaseDeviceVolume(int)} (int)} instead.
*/
@InlineMe(replacement = "this.decreaseDeviceVolume(0)")
@Deprecated
@Override
public void decreaseDeviceVolume() {}
public void decreaseDeviceVolume() {
decreaseDeviceVolume(/* flags= */ 0);
}
/** This method is not supported and does nothing. */
@Override
public void decreaseDeviceVolume(@C.VolumeFlags int flags) {}
public void decreaseDeviceVolume(@C.VolumeFlags int flags) {
setDeviceVolume(getDeviceVolume() - 1, flags);
}
/**
* @deprecated Use {@link #setDeviceMuted(boolean, int)} instead.
*/
@InlineMe(replacement = "this.setDeviceMuted(muted, 0)")
@Deprecated
@Override
public void setDeviceMuted(boolean muted) {}
public void setDeviceMuted(boolean muted) {
setDeviceMuted(muted, /* flags= */ 0);
}
/** This method is not supported and does nothing. */
@Override
public void setDeviceMuted(boolean muted, @C.VolumeFlags int flags) {}
public void setDeviceMuted(boolean muted, @C.VolumeFlags int flags) {
if (castSession == null) {
return;
}
try {
castSession.setMute(muted);
} catch (IOException e) {
Log.w(TAG, "Ignoring setDeviceMuted due to exception", e);
return;
}
setDeviceVolumeAndNotifyIfChanged(deviceVolume, muted);
listeners.flushEvents();
}
/** This method is not supported and does nothing. */
@Override
@ -906,6 +969,7 @@ public final class CastPlayer extends BasePlayer {
? getCurrentTimeline().getPeriod(oldWindowIndex, period, /* setIds= */ true).uid
: null;
updatePlayerStateAndNotifyIfChanged(/* resultCallback= */ null);
updateVolumeAndNotifyIfChanged();
updateRepeatModeAndNotifyIfChanged(/* resultCallback= */ null);
updatePlaybackRateAndNotifyIfChanged(/* resultCallback= */ null);
boolean playingPeriodChangedByTimelineChange = updateTimelineAndNotifyIfChanged();
@ -1014,6 +1078,14 @@ public final class CastPlayer extends BasePlayer {
}
}
@RequiresNonNull("castSession")
private void updateVolumeAndNotifyIfChanged() {
if (castSession != null) {
int deviceVolume = VOLUME_RANGE.clamp((int) Math.round(castSession.getVolume() * MAX_VOLUME));
setDeviceVolumeAndNotifyIfChanged(deviceVolume, castSession.isMute());
}
}
@RequiresNonNull("remoteMediaClient")
private void updateRepeatModeAndNotifyIfChanged(@Nullable ResultCallback<?> resultCallback) {
if (repeatMode.acceptsUpdate(resultCallback)) {
@ -1255,6 +1327,17 @@ public final class CastPlayer extends BasePlayer {
/* adIndexInAdGroup= */ C.INDEX_UNSET);
}
private void setDeviceVolumeAndNotifyIfChanged(
@IntRange(from = 0) int deviceVolume, boolean isMuted) {
if (this.deviceVolume != deviceVolume || this.isMuted != isMuted) {
this.deviceVolume = deviceVolume;
this.isMuted = isMuted;
listeners.queueEvent(
Player.EVENT_DEVICE_VOLUME_CHANGED,
listener -> listener.onDeviceVolumeChanged(deviceVolume, isMuted));
}
}
private void setRepeatModeAndNotifyIfChanged(@Player.RepeatMode int repeatMode) {
if (this.repeatMode.value != repeatMode) {
this.repeatMode.value = repeatMode;
@ -1307,7 +1390,16 @@ public final class CastPlayer extends BasePlayer {
}
}
private void setRemoteMediaClient(@Nullable RemoteMediaClient remoteMediaClient) {
private void setCastSession(@Nullable CastSession castSession) {
if (this.castSession != null) {
this.castSession.removeCastListener(castListener);
}
if (castSession != null) {
castSession.addCastListener(castListener);
}
this.castSession = castSession;
RemoteMediaClient remoteMediaClient =
castSession != null ? castSession.getRemoteMediaClient() : null;
if (this.remoteMediaClient == remoteMediaClient) {
// Do nothing.
return;
@ -1468,22 +1560,22 @@ public final class CastPlayer extends BasePlayer {
@Override
public void onSessionStarted(CastSession castSession, String s) {
setRemoteMediaClient(castSession.getRemoteMediaClient());
setCastSession(castSession);
}
@Override
public void onSessionResumed(CastSession castSession, boolean b) {
setRemoteMediaClient(castSession.getRemoteMediaClient());
setCastSession(castSession);
}
@Override
public void onSessionEnded(CastSession castSession, int i) {
setRemoteMediaClient(null);
setCastSession(null);
}
@Override
public void onSessionSuspended(CastSession castSession, int i) {
setRemoteMediaClient(null);
setCastSession(null);
}
@Override
@ -1645,6 +1737,7 @@ public final class CastPlayer extends BasePlayer {
// TODO b/364580007 - Populate volume information, and implement Player volume-related
// methods.
return new DeviceInfo.Builder(DeviceInfo.PLAYBACK_TYPE_REMOTE)
.setMaxVolume(MAX_VOLUME)
.setRoutingControllerId(remoteController.getId())
.build();
}
@ -1676,4 +1769,13 @@ public final class CastPlayer extends BasePlayer {
}
}
}
private final class CastListener extends Cast.Listener {
@Override
public void onVolumeChanged() {
updateVolumeAndNotifyIfChanged();
listeners.flushEvents();
}
}
}

View File

@ -72,6 +72,7 @@ import androidx.media3.common.Player.Listener;
import androidx.media3.common.Timeline;
import androidx.media3.common.util.Assertions;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.gms.cast.Cast;
import com.google.android.gms.cast.MediaInfo;
import com.google.android.gms.cast.MediaQueueItem;
import com.google.android.gms.cast.MediaStatus;
@ -83,6 +84,7 @@ import com.google.android.gms.cast.framework.media.RemoteMediaClient;
import com.google.android.gms.common.api.PendingResult;
import com.google.android.gms.common.api.ResultCallback;
import com.google.common.collect.ImmutableList;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
@ -102,6 +104,7 @@ public class CastPlayerTest {
private CastPlayer castPlayer;
private DefaultMediaItemConverter mediaItemConverter;
private Cast.Listener castListener;
private RemoteMediaClient.Callback remoteMediaClientCallback;
@Mock private RemoteMediaClient mockRemoteMediaClient;
@ -117,6 +120,7 @@ public class CastPlayerTest {
private ArgumentCaptor<ResultCallback<RemoteMediaClient.MediaChannelResult>>
setResultCallbackArgumentCaptor;
@Captor private ArgumentCaptor<Cast.Listener> castListenerArgumentCaptor;
@Captor private ArgumentCaptor<RemoteMediaClient.Callback> callbackArgumentCaptor;
@Captor private ArgumentCaptor<MediaQueueItem[]> queueItemsArgumentCaptor;
@Captor private ArgumentCaptor<MediaItem> mediaItemCaptor;
@ -139,6 +143,8 @@ public class CastPlayerTest {
mediaItemConverter = new DefaultMediaItemConverter();
castPlayer = new CastPlayer(mockCastContext, mediaItemConverter);
castPlayer.addListener(mockListener);
verify(mockCastSession).addCastListener(castListenerArgumentCaptor.capture());
castListener = castListenerArgumentCaptor.getValue();
verify(mockRemoteMediaClient).registerCallback(callbackArgumentCaptor.capture());
remoteMediaClientCallback = callbackArgumentCaptor.getValue();
}
@ -1398,10 +1404,10 @@ public class CastPlayerTest {
assertThat(castPlayer.isCommandAvailable(COMMAND_SET_MEDIA_ITEM)).isTrue();
assertThat(castPlayer.isCommandAvailable(COMMAND_GET_AUDIO_ATTRIBUTES)).isFalse();
assertThat(castPlayer.isCommandAvailable(COMMAND_GET_VOLUME)).isFalse();
assertThat(castPlayer.isCommandAvailable(COMMAND_GET_DEVICE_VOLUME)).isFalse();
assertThat(castPlayer.isCommandAvailable(COMMAND_GET_DEVICE_VOLUME)).isTrue();
assertThat(castPlayer.isCommandAvailable(COMMAND_SET_VOLUME)).isFalse();
assertThat(castPlayer.isCommandAvailable(COMMAND_SET_DEVICE_VOLUME)).isFalse();
assertThat(castPlayer.isCommandAvailable(COMMAND_ADJUST_DEVICE_VOLUME)).isFalse();
assertThat(castPlayer.isCommandAvailable(COMMAND_SET_DEVICE_VOLUME)).isTrue();
assertThat(castPlayer.isCommandAvailable(COMMAND_ADJUST_DEVICE_VOLUME)).isTrue();
assertThat(castPlayer.isCommandAvailable(COMMAND_SET_VIDEO_SURFACE)).isFalse();
assertThat(castPlayer.isCommandAvailable(COMMAND_GET_TEXT)).isFalse();
assertThat(castPlayer.isCommandAvailable(Player.COMMAND_RELEASE)).isTrue();
@ -1935,6 +1941,40 @@ public class CastPlayerTest {
assertThat(deviceInfo.playbackType).isEqualTo(DeviceInfo.PLAYBACK_TYPE_REMOTE);
}
@Test
public void setDeviceVolume_updatesCastSessionVolume() throws IOException {
int maxVolume = castPlayer.getDeviceInfo().maxVolume;
int volumeToSet = 10;
castPlayer.addListener(mockListener);
castPlayer.setDeviceVolume(volumeToSet, /* flags= */ 0);
verify(mockListener, times(1)).onDeviceVolumeChanged(volumeToSet, /* muted= */ false);
verify(mockCastSession).setVolume((double) volumeToSet / maxVolume);
assertThat(castPlayer.getDeviceVolume()).isEqualTo(volumeToSet);
double newCastSessionVolume = .25;
int expectedDeviceVolume = (int) (newCastSessionVolume * maxVolume);
when(mockCastSession.getVolume()).thenReturn(newCastSessionVolume);
castListener.onVolumeChanged();
assertThat(castPlayer.getDeviceVolume()).isEqualTo(expectedDeviceVolume);
verify(mockListener, times(1)).onDeviceVolumeChanged(volumeToSet, /* muted= */ false);
}
@Test
public void setDeviceMuted_mutesCastSession() throws IOException {
castPlayer.addListener(mockListener);
castPlayer.setDeviceMuted(true, /* flags= */ 0);
verify(mockListener, times(1)).onDeviceVolumeChanged(0, /* muted= */ true);
verify(mockCastSession).setMute(true);
assertThat(castPlayer.isDeviceMuted()).isEqualTo(true);
when(mockCastSession.isMute()).thenReturn(false);
castListener.onVolumeChanged();
assertThat(castPlayer.isDeviceMuted()).isEqualTo(false);
verify(mockListener, times(1)).onDeviceVolumeChanged(0, /* muted= */ false);
}
private int[] createMediaQueueItemIds(int numberOfIds) {
int[] mediaQueueItemIds = new int[numberOfIds];
for (int i = 0; i < numberOfIds; i++) {

View File

@ -1057,12 +1057,13 @@ public interface ExoPlayer extends Player {
*
* @param playbackLooper A {@link Looper}.
* @return This builder.
* @throws IllegalStateException If {@link #build()} has already been called.
* @throws IllegalStateException If {@link #build()} has already been called, or when the
* {@linkplain Looper#getMainLooper() main looper} is passed in.
*/
@CanIgnoreReturnValue
@UnstableApi
public Builder setPlaybackLooper(Looper playbackLooper) {
checkState(!buildCalled);
checkState(!buildCalled && playbackLooper != Looper.getMainLooper());
this.playbackLooperProvider = new PlaybackLooperProvider(playbackLooper);
return this;
}

View File

@ -2651,7 +2651,8 @@ import java.util.Objects;
hasSecondaryRenderers && !isPrewarmingDisabledUntilNextTransition;
if (arePrewarmingRenderersHandlingDiscontinuity) {
for (int i = 0; i < renderers.length; i++) {
if (!newTrackSelectorResult.isRendererEnabled(i)) {
if (!newTrackSelectorResult.isRendererEnabled(i)
|| renderers[i].getTrackType() == C.TRACK_TYPE_NONE) {
continue;
}
// TODO: This check should ideally be replaced by a per-stream discontinuity check

View File

@ -23,6 +23,7 @@ import androidx.media3.common.Format;
import androidx.media3.common.audio.AudioProcessor;
import androidx.media3.common.audio.BaseAudioProcessor;
import androidx.media3.common.util.Assertions;
import androidx.media3.common.util.UnstableApi;
import java.nio.ByteBuffer;
import java.util.Arrays;
@ -30,7 +31,8 @@ import java.util.Arrays;
* An {@link AudioProcessor} that applies a mapping from input channels onto specified output
* channels. This can be used to reorder, duplicate or discard channels.
*/
/* package */ final class ChannelMappingAudioProcessor extends BaseAudioProcessor {
@UnstableApi
public final class ChannelMappingAudioProcessor extends BaseAudioProcessor {
@Nullable private int[] pendingOutputChannels;
@Nullable private int[] outputChannels;

View File

@ -62,7 +62,7 @@ public final class DefaultAudioOffloadSupportProvider
* offload variable rate support.
*/
public DefaultAudioOffloadSupportProvider(@Nullable Context context) {
this.context = context;
this.context = context == null ? null : context.getApplicationContext();
}
@Override

View File

@ -592,7 +592,7 @@ public final class DefaultAudioSink implements AudioSink {
@RequiresNonNull("#1.audioProcessorChain")
private DefaultAudioSink(Builder builder) {
context = builder.context;
context = builder.context == null ? null : builder.context.getApplicationContext();
audioAttributes = AudioAttributes.DEFAULT;
audioCapabilities = context != null ? null : builder.audioCapabilities;
audioProcessorChain = builder.audioProcessorChain;

View File

@ -19,6 +19,7 @@ import androidx.media3.common.C;
import androidx.media3.common.Format;
import androidx.media3.common.audio.AudioProcessor;
import androidx.media3.common.audio.BaseAudioProcessor;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.common.util.Util;
import java.nio.ByteBuffer;
@ -34,7 +35,8 @@ import java.nio.ByteBuffer;
* <li>{@link C#ENCODING_PCM_FLOAT} ({@link #isActive()} will return {@code false})
* </ul>
*/
/* package */ final class ToFloatPcmAudioProcessor extends BaseAudioProcessor {
@UnstableApi
public final class ToFloatPcmAudioProcessor extends BaseAudioProcessor {
private static final int FLOAT_NAN_AS_INT = Float.floatToIntBits(Float.NaN);
private static final double PCM_32_BIT_INT_TO_PCM_32_BIT_FLOAT_FACTOR = 1.0 / 0x7FFFFFFF;

View File

@ -20,11 +20,13 @@ import static java.lang.Math.min;
import androidx.media3.common.C;
import androidx.media3.common.Format;
import androidx.media3.common.audio.BaseAudioProcessor;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.common.util.Util;
import java.nio.ByteBuffer;
/** Audio processor for trimming samples from the start/end of data. */
/* package */ final class TrimmingAudioProcessor extends BaseAudioProcessor {
@UnstableApi
public final class TrimmingAudioProcessor extends BaseAudioProcessor {
private int trimStartFrames;
private int trimEndFrames;

View File

@ -197,11 +197,12 @@ public final class DefaultPreloadManager extends BasePreloadManager<Integer> {
* @param preloadLooper A {@link Looper}.
* @return This builder.
* @throws IllegalStateException If {@link #build()}, {@link #buildExoPlayer()} or {@link
* #buildExoPlayer(ExoPlayer.Builder)} has already been called.
* #buildExoPlayer(ExoPlayer.Builder)} has already been called, or when the {@linkplain
* Looper#getMainLooper() main looper} is passed in.
*/
@CanIgnoreReturnValue
public Builder setPreloadLooper(Looper preloadLooper) {
checkState(!buildCalled && !buildExoPlayerCalled);
checkState(!buildCalled && !buildExoPlayerCalled && preloadLooper != Looper.getMainLooper());
this.preloadLooperProvider = new PlaybackLooperProvider(preloadLooper);
return this;
}

View File

@ -679,9 +679,9 @@ public final class ExoPlayerTest {
@Test
public void renderersLifecycle_onlyRenderersThatAreEnabled_areSetToFinal() throws Exception {
AtomicInteger videoStreamSetToFinalCount = new AtomicInteger();
final FakeRenderer videoRenderer = new FakeRenderer(C.TRACK_TYPE_VIDEO);
final FakeRenderer audioRenderer = new FakeRenderer(C.TRACK_TYPE_AUDIO);
final ForwardingRenderer forwardingVideoRenderer =
FakeRenderer videoRenderer = new FakeRenderer(C.TRACK_TYPE_VIDEO);
FakeRenderer audioRenderer = new FakeRenderer(C.TRACK_TYPE_AUDIO);
ForwardingRenderer forwardingVideoRenderer =
new ForwardingRenderer(videoRenderer) {
@Override
public void setCurrentStreamFinal() {
@ -694,18 +694,18 @@ public final class ExoPlayerTest {
new TestExoPlayerBuilder(context)
.setRenderers(forwardingVideoRenderer, audioRenderer))
.build();
// Use media sources with discontinuities so that enabled streams are set to final.
// Use media sources with discontinuities so that enabled streams are set to final. Also ensure
// to use Formats that are not guaranteed to be only sync samples, so ClippingMediaSource
// maintains the initial discontinuity.
Format videoFormat = new Format.Builder().setSampleMimeType(MimeTypes.VIDEO_AV1).build();
Format audioFormat = new Format.Builder().setSampleMimeType(MimeTypes.AUDIO_DTS).build();
ClippingMediaSource clippedFakeAudioSource =
new ClippingMediaSource.Builder(
new FakeMediaSource(new FakeTimeline(), ExoPlayerTestRunner.AUDIO_FORMAT))
new ClippingMediaSource.Builder(new FakeMediaSource(new FakeTimeline(), audioFormat))
.setEndPositionMs(300)
.build();
ClippingMediaSource clippedFakeAudioVideoSource =
new ClippingMediaSource.Builder(
new FakeMediaSource(
new FakeTimeline(),
ExoPlayerTestRunner.VIDEO_FORMAT,
ExoPlayerTestRunner.AUDIO_FORMAT))
new FakeMediaSource(new FakeTimeline(), videoFormat, audioFormat))
.setEndPositionMs(300)
.build();
player.setMediaSources(

View File

@ -67,6 +67,7 @@ import androidx.media3.test.utils.robolectric.ShadowMediaCodecConfig;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.common.collect.ImmutableList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
@ -1609,6 +1610,56 @@ public class ExoPlayerWithPrewarmingRenderersTest {
assertThat(secondaryVideoState3).isEqualTo(Renderer.STATE_STARTED);
}
@Test
public void
play_withNoSampleRendererAndInitialDiscontinuity_usesPrewarmingAndTransitionsWithoutError()
throws Exception {
Clock fakeClock = new FakeClock(/* isAutoAdvancing= */ true);
RenderersFactory renderersFactoryWithNoSampleRenderer =
new FakeRenderersFactorySupportingSecondaryVideoRenderer(
fakeClock, /* includeNoSampleRenderer= */ true);
ExoPlayer player =
new TestExoPlayerBuilder(context)
.setClock(fakeClock)
.setRenderersFactory(renderersFactoryWithNoSampleRenderer)
.build();
Renderer secondaryVideoRenderer = player.getSecondaryRenderer(/* index= */ 0);
Renderer noSampleRenderer = player.getRenderer(/* index= */ 2);
assertThat(secondaryVideoRenderer.getTrackType()).isEqualTo(C.TRACK_TYPE_VIDEO);
assertThat(noSampleRenderer.getTrackType()).isEqualTo(C.TRACK_TYPE_NONE);
// Set a playlist that allows a new renderer to be enabled early and uses an initial
// discontinuity.
player.setMediaSources(
ImmutableList.of(
new FakeMediaSource(new FakeTimeline(), ExoPlayerTestRunner.VIDEO_FORMAT),
new FakeMediaSource(
new FakeTimeline(),
ExoPlayerTestRunner.VIDEO_FORMAT,
ExoPlayerTestRunner.AUDIO_FORMAT) {
@Override
public MediaPeriod createPeriod(
MediaPeriodId id, Allocator allocator, long startPositionUs) {
FakeMediaPeriod fakeMediaPeriod =
(FakeMediaPeriod) super.createPeriod(id, allocator, startPositionUs);
fakeMediaPeriod.setDiscontinuityPositionUs(100);
return fakeMediaPeriod;
}
}));
player.prepare();
// Play until secondary video renderer item is started, verifying its being used for prewarming.
// This step should not have any influence on the NoSampleRenderer and it should not throw.
player.play();
advance(player)
.untilBackgroundThreadCondition(
() -> secondaryVideoRenderer.getState() == Renderer.STATE_STARTED);
@Renderer.State int noSampleRendererState = noSampleRenderer.getState();
advance(player).untilState(Player.STATE_ENDED);
player.release();
assertThat(noSampleRendererState).isEqualTo(Renderer.STATE_STARTED);
}
/** {@link FakeMediaSource} that prevents any reading of samples off the sample queue. */
private static final class FakeBlockingMediaSource extends FakeMediaSource {
@ -1670,9 +1721,16 @@ public class ExoPlayerWithPrewarmingRenderersTest {
private static class FakeRenderersFactorySupportingSecondaryVideoRenderer
implements RenderersFactory {
protected final Clock clock;
private final boolean includeNoSampleRenderer;
public FakeRenderersFactorySupportingSecondaryVideoRenderer(Clock clock) {
this(clock, /* includeNoSampleRenderer= */ false);
}
public FakeRenderersFactorySupportingSecondaryVideoRenderer(
Clock clock, boolean includeNoSampleRenderer) {
this.clock = clock;
this.includeNoSampleRenderer = includeNoSampleRenderer;
}
@Override
@ -1684,10 +1742,25 @@ public class ExoPlayerWithPrewarmingRenderersTest {
MetadataOutput metadataRendererOutput) {
HandlerWrapper clockAwareHandler =
clock.createHandler(eventHandler.getLooper(), /* callback= */ null);
return new Renderer[] {
new FakeVideoRenderer(clockAwareHandler, videoRendererEventListener),
new FakeAudioRenderer(clockAwareHandler, audioRendererEventListener)
};
Renderer[] renderers =
new Renderer[] {
new FakeVideoRenderer(clockAwareHandler, videoRendererEventListener),
new FakeAudioRenderer(clockAwareHandler, audioRendererEventListener)
};
if (includeNoSampleRenderer) {
renderers = Arrays.copyOf(renderers, renderers.length + 1);
renderers[renderers.length - 1] =
new NoSampleRenderer() {
@Override
public String getName() {
return "NoSampleRenderer";
}
@Override
public void render(long positionUs, long elapsedRealtimeUs) {}
};
}
return renderers;
}
@Override

View File

@ -30,7 +30,6 @@ import static org.robolectric.Shadows.shadowOf;
import android.content.Context;
import android.net.Uri;
import android.os.HandlerThread;
import android.os.Looper;
import androidx.annotation.Nullable;
import androidx.media3.common.AdPlaybackState;
import androidx.media3.common.C;
@ -68,7 +67,9 @@ import com.google.common.collect.Iterables;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicInteger;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@ -81,6 +82,7 @@ public class DefaultPreloadManagerTest {
private Context context;
@Mock private TargetPreloadStatusControl<Integer> mockTargetPreloadStatusControl;
private RenderersFactory renderersFactory;
private HandlerThread preloadThread;
@Before
public void setUp() {
@ -95,6 +97,13 @@ public class DefaultPreloadManagerTest {
SystemClock.DEFAULT.createHandler(handler.getLooper(), /* callback= */ null),
audioListener)
};
preloadThread = new HandlerThread("DefaultPreloadManagerTest");
preloadThread.start();
}
@After
public void tearDown() {
preloadThread.quit();
}
@Test
@ -102,7 +111,7 @@ public class DefaultPreloadManagerTest {
DefaultPreloadManager preloadManager =
new DefaultPreloadManager.Builder(context, mockTargetPreloadStatusControl)
.setRenderersFactory(renderersFactory)
.setPreloadLooper(Util.getCurrentOrMainLooper())
.setPreloadLooper(preloadThread.getLooper())
.build();
MediaItem.Builder mediaItemBuilder = new MediaItem.Builder();
MediaItem mediaItem1 =
@ -123,7 +132,7 @@ public class DefaultPreloadManagerTest {
DefaultPreloadManager preloadManager =
new DefaultPreloadManager.Builder(context, mockTargetPreloadStatusControl)
.setRenderersFactory(renderersFactory)
.setPreloadLooper(Util.getCurrentOrMainLooper())
.setPreloadLooper(preloadThread.getLooper())
.build();
MediaItem.Builder mediaItemBuilder = new MediaItem.Builder();
MediaItem mediaItem1 =
@ -148,7 +157,7 @@ public class DefaultPreloadManagerTest {
DefaultPreloadManager preloadManager =
new DefaultPreloadManager.Builder(context, mockTargetPreloadStatusControl)
.setRenderersFactory(renderersFactory)
.setPreloadLooper(Util.getCurrentOrMainLooper())
.setPreloadLooper(preloadThread.getLooper())
.build();
MediaItem mediaItem =
new MediaItem.Builder()
@ -178,8 +187,6 @@ public class DefaultPreloadManagerTest {
ProgressiveMediaSource.Factory mediaSourceFactory =
new ProgressiveMediaSource.Factory(
new DefaultDataSource.Factory(ApplicationProvider.getApplicationContext()));
HandlerThread preloadThread = new HandlerThread("preload");
preloadThread.start();
DefaultPreloadManager preloadManager =
new DefaultPreloadManager.Builder(context, targetPreloadStatusControl)
.setMediaSourceFactory(mediaSourceFactory)
@ -216,8 +223,6 @@ public class DefaultPreloadManagerTest {
assertThat(preloadManagerListener.onCompletedMediaItemRecords)
.containsExactly(mediaItem0, mediaItem1, mediaItem2)
.inOrder();
preloadThread.quit();
}
@Test
@ -595,7 +600,7 @@ public class DefaultPreloadManagerTest {
new DefaultPreloadManager.Builder(context, targetPreloadStatusControl)
.setMediaSourceFactory(mockMediaSourceFactory)
.setRenderersFactory(renderersFactory)
.setPreloadLooper(Util.getCurrentOrMainLooper())
.setPreloadLooper(preloadThread.getLooper())
.build();
MediaItem.Builder mediaItemBuilder = new MediaItem.Builder();
MediaItem mediaItem0 =
@ -667,7 +672,7 @@ public class DefaultPreloadManagerTest {
new DefaultPreloadManager.Builder(context, targetPreloadStatusControl)
.setMediaSourceFactory(mockMediaSourceFactory)
.setRenderersFactory(renderersFactory)
.setPreloadLooper(Util.getCurrentOrMainLooper())
.setPreloadLooper(preloadThread.getLooper())
.build();
MediaItem.Builder mediaItemBuilder = new MediaItem.Builder();
MediaItem mediaItem1 =
@ -694,11 +699,11 @@ public class DefaultPreloadManagerTest {
});
preloadManager.add(mediaItem1, /* rankingData= */ 1);
preloadManager.invalidate();
shadowOf(Looper.getMainLooper()).idle();
shadowOf(preloadThread.getLooper()).idle();
boolean mediaItem1Removed = preloadManager.remove(mediaItem1);
boolean mediaItem2Removed = preloadManager.remove(mediaItem2);
shadowOf(Looper.getMainLooper()).idle();
shadowOf(preloadThread.getLooper()).idle();
assertThat(mediaItem1Removed).isTrue();
assertThat(mediaItem2Removed).isFalse();
@ -715,7 +720,7 @@ public class DefaultPreloadManagerTest {
new DefaultPreloadManager.Builder(context, targetPreloadStatusControl)
.setMediaSourceFactory(mockMediaSourceFactory)
.setRenderersFactory(renderersFactory)
.setPreloadLooper(Util.getCurrentOrMainLooper())
.setPreloadLooper(preloadThread.getLooper())
.build();
MediaItem.Builder mediaItemBuilder = new MediaItem.Builder();
MediaItem mediaItem1 =
@ -742,7 +747,7 @@ public class DefaultPreloadManagerTest {
});
preloadManager.add(mediaItem1, /* rankingData= */ 1);
preloadManager.invalidate();
shadowOf(Looper.getMainLooper()).idle();
shadowOf(preloadThread.getLooper()).idle();
MediaSource mediaSource1 = preloadManager.getMediaSource(mediaItem1);
DefaultMediaSourceFactory defaultMediaSourceFactory =
new DefaultMediaSourceFactory((Context) ApplicationProvider.getApplicationContext());
@ -752,7 +757,7 @@ public class DefaultPreloadManagerTest {
boolean mediaSource1Removed = preloadManager.remove(mediaSource1);
boolean mediaSource2Removed = preloadManager.remove(mediaSource2);
boolean mediaSource3Removed = preloadManager.remove(mediaSource3);
shadowOf(Looper.getMainLooper()).idle();
shadowOf(preloadThread.getLooper()).idle();
assertThat(mediaSource1Removed).isTrue();
assertThat(mediaSource2Removed).isFalse();
@ -762,7 +767,8 @@ public class DefaultPreloadManagerTest {
}
@Test
public void reset_returnZeroCount_sourcesButNotRendererCapabilitiesListReleased() {
public void reset_returnZeroCount_sourcesButNotRendererCapabilitiesListReleased()
throws TimeoutException {
TargetPreloadStatusControl<Integer> targetPreloadStatusControl =
rankingData -> new DefaultPreloadManager.Status(STAGE_SOURCE_PREPARED);
MediaSource.Factory mockMediaSourceFactory = mock(MediaSource.Factory.class);
@ -789,7 +795,7 @@ public class DefaultPreloadManagerTest {
new DefaultPreloadManager.Builder(context, targetPreloadStatusControl)
.setMediaSourceFactory(mockMediaSourceFactory)
.setRenderersFactory(renderersFactory)
.setPreloadLooper(Util.getCurrentOrMainLooper())
.setPreloadLooper(preloadThread.getLooper())
.build();
MediaItem.Builder mediaItemBuilder = new MediaItem.Builder();
MediaItem mediaItem1 =
@ -817,10 +823,11 @@ public class DefaultPreloadManagerTest {
preloadManager.add(mediaItem1, /* rankingData= */ 1);
preloadManager.add(mediaItem2, /* rankingData= */ 2);
preloadManager.invalidate();
shadowOf(Looper.getMainLooper()).idle();
shadowOf(preloadThread.getLooper()).idle();
shadowOf(Util.getCurrentOrMainLooper()).idle();
preloadManager.reset();
shadowOf(Looper.getMainLooper()).idle();
shadowOf(preloadThread.getLooper()).idle();
assertThat(preloadManager.getSourceCount()).isEqualTo(0);
assertThat(internalSourceToReleaseReferenceByMediaId).containsExactly("mediaId1", "mediaId2");
@ -888,7 +895,7 @@ public class DefaultPreloadManagerTest {
preloadManager.add(mediaItem2, /* rankingData= */ 2);
preloadManager.invalidate();
shadowOf(preloadThread.getLooper()).idle();
shadowOf(Looper.getMainLooper()).idle();
shadowOf(Util.getCurrentOrMainLooper()).idle();
preloadManager.release();
shadowOf(preloadThread.getLooper()).idle();

View File

@ -70,6 +70,7 @@ public final class ExoPlayerTestRunner implements Player.Listener, ActionSchedul
public static final Format AUDIO_FORMAT =
new Format.Builder()
.setSampleMimeType(MimeTypes.AUDIO_AAC)
.setCodecs("mp4a.40.2")
.setAverageBitrate(100_000)
.setChannelCount(2)
.setSampleRate(44100)

View File

@ -62,6 +62,7 @@ import java.util.List;
import java.util.concurrent.TimeoutException;
import org.junit.After;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
@ -110,6 +111,7 @@ public class ReplayCacheTest {
}
@Test
@Ignore("TODO: b/391109644 - Fix this test and re-enable it")
public void replayOnEveryFrame_withExoPlayer_succeeds()
throws PlaybackException, TimeoutException {
assumeTrue(