diff --git a/libraries/common/src/main/java/androidx/media3/common/Format.java b/libraries/common/src/main/java/androidx/media3/common/Format.java
index 32da706cec..68838ea29c 100644
--- a/libraries/common/src/main/java/androidx/media3/common/Format.java
+++ b/libraries/common/src/main/java/androidx/media3/common/Format.java
@@ -19,6 +19,8 @@
import static com.google.common.math.DoubleMath.fuzzyEquals;
import static java.lang.annotation.ElementType.TYPE_USE;
+import android.media.AudioPresentation;
+import android.os.Build;
import android.os.Bundle;
import android.text.TextUtils;
import androidx.annotation.IntDef;
@@ -116,6 +118,7 @@
*
{@link #pcmEncoding}
* {@link #encoderDelay}
* {@link #encoderPadding}
+ * {@link #audioPresentations}
*
*
* Fields relevant to text formats
@@ -193,7 +196,7 @@ public static final class Builder {
private @C.PcmEncoding int pcmEncoding;
private int encoderDelay;
private int encoderPadding;
-
+ private List audioPresentations;
// Text specific.
private int accessibilityChannel;
@@ -239,6 +242,7 @@ public Builder() {
// Provided by the source.
cryptoType = C.CRYPTO_TYPE_NONE;
auxiliaryTrackType = C.AUXILIARY_TRACK_TYPE_UNDEFINED;
+ audioPresentations = ImmutableList.of();
}
/**
@@ -286,6 +290,7 @@ private Builder(Format format) {
this.pcmEncoding = format.pcmEncoding;
this.encoderDelay = format.encoderDelay;
this.encoderPadding = format.encoderPadding;
+ this.audioPresentations = format.audioPresentations;
// Text specific.
this.accessibilityChannel = format.accessibilityChannel;
this.cueReplacementBehavior = format.cueReplacementBehavior;
@@ -766,6 +771,18 @@ public Builder setEncoderPadding(int encoderPadding) {
return this;
}
+ /**
+ * Sets {@link AudioPresentation}. The default value is {@code null}.
+ *
+ * @param presentations The {@link Format#audioPresentations}.
+ * @return The builder.
+ */
+ @CanIgnoreReturnValue
+ public Builder setAudioPresentations(List presentations) {
+ this.audioPresentations = ImmutableList.copyOf(presentations);
+ return this;
+ }
+
// Text specific.
/**
@@ -1104,6 +1121,11 @@ public Format build() {
*/
@UnstableApi public final int encoderPadding;
+ /** The audio presentations. Will not be null, but may be empty if the container doesn't have
+ * audio presentations in it.
+ */
+ public final List audioPresentations;
+
// Text specific.
/** The Accessibility channel, or {@link #NO_VALUE} if not known or applicable. */
@@ -1209,6 +1231,7 @@ private Format(Builder builder) {
pcmEncoding = builder.pcmEncoding;
encoderDelay = builder.encoderDelay == NO_VALUE ? 0 : builder.encoderDelay;
encoderPadding = builder.encoderPadding == NO_VALUE ? 0 : builder.encoderPadding;
+ audioPresentations = builder.audioPresentations;
// Text specific.
accessibilityChannel = builder.accessibilityChannel;
cueReplacementBehavior = builder.cueReplacementBehavior;
@@ -1348,6 +1371,8 @@ public String toString() {
+ channelCount
+ ", "
+ sampleRate
+ + ", "
+ + audioPresentations
+ "])";
}
@@ -1394,6 +1419,7 @@ public int hashCode() {
result = 31 * result + pcmEncoding;
result = 31 * result + encoderDelay;
result = 31 * result + encoderPadding;
+ // [Omitted audioPresentations]
// Text specific.
result = 31 * result + accessibilityChannel;
// Image specific.
@@ -1447,6 +1473,7 @@ public boolean equals(@Nullable Object obj) {
&& Objects.equals(id, other.id)
&& Objects.equals(label, other.label)
&& labels.equals(other.labels)
+ && audioPresentationsEquals(other)
&& Objects.equals(codecs, other.codecs)
&& Objects.equals(containerMimeType, other.containerMimeType)
&& Objects.equals(sampleMimeType, other.sampleMimeType)
@@ -1480,6 +1507,27 @@ public boolean initializationDataEquals(Format other) {
return true;
}
+ /**
+ * Returns whether the {@link #audioPresentations}s belonging to this format and {@code other} are
+ * equal.
+ *
+ * @param other The other format whose {@link #audioPresentations} is being compared.
+ * @return Whether the {@link #audioPresentations}s belonging to this format and {@code other} are
+ * equal.
+ */
+ @UnstableApi
+ public boolean audioPresentationsEquals(Format other) {
+ if (audioPresentations.size() != other.audioPresentations.size()) {
+ return false;
+ }
+ for (int i = 0; i < audioPresentations.size(); i++) {
+ if (!audioPresentations.get(i).equals(other.audioPresentations.get(i))) {
+ return false;
+ }
+ }
+ return true;
+ }
+
// Utility methods
/** Returns a prettier {@link String} than {@link #toString()}, intended for logging. */
@@ -1618,6 +1666,7 @@ public static String toLogString(@Nullable Format format) {
private static final String FIELD_MAX_SUB_LAYERS = Util.intToStringMaxRadix(34);
private static final String FIELD_DECODED_WIDTH = Util.intToStringMaxRadix(35);
private static final String FIELD_DECODED_HEIGHT = Util.intToStringMaxRadix(36);
+ private static final String FIELD_AUDIO_PRESENTATIONS = Util.intToStringMaxRadix(37);
/**
* Returns a {@link Bundle} representing the information stored in this object. If {@code
@@ -1672,6 +1721,11 @@ public Bundle toBundle() {
bundle.putInt(FIELD_PCM_ENCODING, pcmEncoding);
bundle.putInt(FIELD_ENCODER_DELAY, encoderDelay);
bundle.putInt(FIELD_ENCODER_PADDING, encoderPadding);
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
+ for (int i = 0; i < audioPresentations.size(); i++) {
+ bundle.putParcelable(keyForAudioPresentations(i), audioPresentations.get(i));
+ }
+ }
// Text specific.
bundle.putInt(FIELD_ACCESSIBILITY_CHANNEL, accessibilityChannel);
// Image specific.
@@ -1742,12 +1796,21 @@ public static Format fromBundle(Bundle bundle) {
builder.setColorInfo(ColorInfo.fromBundle(colorInfoBundle));
}
// Audio specific.
+ List presentations = new ArrayList<>();
+ for (int i = 0; ; i++) {
+ @Nullable AudioPresentation data = bundle.getParcelable(keyForAudioPresentations(i));
+ if (data == null) {
+ break;
+ }
+ presentations.add(data);
+ }
builder
.setChannelCount(bundle.getInt(FIELD_CHANNEL_COUNT, DEFAULT.channelCount))
.setSampleRate(bundle.getInt(FIELD_SAMPLE_RATE, DEFAULT.sampleRate))
.setPcmEncoding(bundle.getInt(FIELD_PCM_ENCODING, DEFAULT.pcmEncoding))
.setEncoderDelay(bundle.getInt(FIELD_ENCODER_DELAY, DEFAULT.encoderDelay))
.setEncoderPadding(bundle.getInt(FIELD_ENCODER_PADDING, DEFAULT.encoderPadding))
+ .setAudioPresentations(presentations)
// Text specific.
.setAccessibilityChannel(
bundle.getInt(FIELD_ACCESSIBILITY_CHANNEL, DEFAULT.accessibilityChannel))
@@ -1767,6 +1830,12 @@ private static String keyForInitializationData(int initialisationDataIndex) {
+ Integer.toString(initialisationDataIndex, Character.MAX_RADIX);
}
+ private static String keyForAudioPresentations(int audioPresentationsIndex) {
+ return FIELD_AUDIO_PRESENTATIONS
+ + "_"
+ + Integer.toString(audioPresentationsIndex, Character.MAX_RADIX);
+ }
+
/**
* Utility method to get {@code defaultValue} if {@code value} is {@code null}. {@code
* defaultValue} can be {@code null}.
diff --git a/libraries/common/src/main/java/androidx/media3/common/Player.java b/libraries/common/src/main/java/androidx/media3/common/Player.java
index f54dc86ca9..0f02438d92 100644
--- a/libraries/common/src/main/java/androidx/media3/common/Player.java
+++ b/libraries/common/src/main/java/androidx/media3/common/Player.java
@@ -21,6 +21,7 @@
import static java.lang.annotation.ElementType.PARAMETER;
import static java.lang.annotation.ElementType.TYPE_USE;
+import android.media.AudioPresentation;
import android.os.Bundle;
import android.os.Looper;
import android.view.Surface;
@@ -30,6 +31,7 @@
import androidx.annotation.FloatRange;
import androidx.annotation.IntDef;
import androidx.annotation.IntRange;
+import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.media3.common.text.Cue;
@@ -859,6 +861,14 @@ default void onMediaItemTransition(
*/
default void onTracksChanged(Tracks tracks) {}
+ /**
+ * Called when the audio presentations, available to the player for the current track, are
+ * changed.
+ *
+ * @param audioPresentations The available audio presentations. Never null, but may be empty.
+ */
+ default void onAudioPresentationsChanged(@NonNull List audioPresentations) {}
+
/**
* Called when the value of {@link Player#getMediaMetadata()} changes.
*
@@ -1539,7 +1549,8 @@ default void onMetadata(Metadata metadata) {}
EVENT_CUES,
EVENT_METADATA,
EVENT_DEVICE_INFO_CHANGED,
- EVENT_DEVICE_VOLUME_CHANGED
+ EVENT_DEVICE_VOLUME_CHANGED,
+ EVENT_AUDIO_PRESENTATIONS_CHANGED
})
@interface Event {}
@@ -1642,6 +1653,9 @@ default void onMetadata(Metadata metadata) {}
/** {@link #getDeviceVolume()} or {@link #isDeviceMuted()} changed. */
int EVENT_DEVICE_VOLUME_CHANGED = 30;
+ /** Audio presentations associated with the current audio track changed. */
+ int EVENT_AUDIO_PRESENTATIONS_CHANGED = 31;
+
/**
* Commands that indicate which method calls are currently permitted on a particular {@code
* Player} instance.
diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayerImpl.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayerImpl.java
index fe2a62b2f0..4124e438de 100644
--- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayerImpl.java
+++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayerImpl.java
@@ -45,6 +45,7 @@
import android.graphics.Rect;
import android.graphics.SurfaceTexture;
import android.media.AudioDeviceInfo;
+import android.media.AudioPresentation;
import android.media.MediaFormat;
import android.os.Handler;
import android.os.Looper;
@@ -153,6 +154,9 @@
private final TrackSelector trackSelector;
private final HandlerWrapper playbackInfoUpdateHandler;
private final ExoPlayerImplInternal.PlaybackInfoUpdateListener playbackInfoUpdateListener;
+ private final HandlerWrapper audioPresentationsUpdateHandler;
+ private final ExoPlayerImplInternal.AudioPresentationsChangeListener
+ audioPresentationsChangeListener;
private final ExoPlayerImplInternal internalPlayer;
private final ListenerSet listeners;
@@ -352,6 +356,12 @@ public ExoPlayerImpl(ExoPlayer.Builder builder, @Nullable Player wrappingPlayer)
playbackInfoUpdate ->
playbackInfoUpdateHandler.post(() -> handlePlaybackInfo(playbackInfoUpdate));
playbackInfo = PlaybackInfo.createDummy(emptyTrackSelectorResult);
+ audioPresentationsUpdateHandler =
+ clock.createHandler(applicationLooper, /* callback= */ null);
+ audioPresentationsChangeListener =
+ audioPresentations ->
+ playbackInfoUpdateHandler.post(() ->
+ handleAudioPresentationsChange(audioPresentations));
analyticsCollector.setPlayer(this.wrappingPlayer, applicationLooper);
PlayerId playerId = new PlayerId(builder.playerName);
internalPlayer =
@@ -377,7 +387,8 @@ public ExoPlayerImpl(ExoPlayer.Builder builder, @Nullable Player wrappingPlayer)
playerId,
builder.playbackLooperProvider,
preloadConfiguration,
- frameMetadataListener);
+ frameMetadataListener,
+ audioPresentationsChangeListener);
Looper playbackLooper = internalPlayer.getPlaybackLooper();
volume = 1;
@@ -463,6 +474,12 @@ public ExoPlayerImpl(ExoPlayer.Builder builder, @Nullable Player wrappingPlayer)
}
}
+ private void handleAudioPresentationsChange(List audioPresentations) {
+ listeners.queueEvent(
+ Player.EVENT_AUDIO_PRESENTATIONS_CHANGED,
+ listener -> listener.onAudioPresentationsChanged(audioPresentations));
+ }
+
@Override
public boolean isSleepingForOffload() {
verifyApplicationThread();
diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayerImplInternal.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayerImplInternal.java
index c899a7a6bf..73a2e81672 100644
--- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayerImplInternal.java
+++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/ExoPlayerImplInternal.java
@@ -28,6 +28,7 @@
import static java.lang.Math.min;
import android.content.Context;
+import android.media.AudioPresentation;
import android.media.MediaFormat;
import android.os.Handler;
import android.os.Looper;
@@ -138,6 +139,10 @@ public interface PlaybackInfoUpdateListener {
void onPlaybackInfoUpdate(ExoPlayerImplInternal.PlaybackInfoUpdate playbackInfo);
}
+ public interface AudioPresentationsChangeListener {
+ void onAudioPresentationsChanged(List audioPresentations);
+ }
+
// Internal messages
private static final int MSG_SET_PLAY_WHEN_READY = 1;
private static final int MSG_DO_SOME_WORK = 2;
@@ -176,6 +181,7 @@ public interface PlaybackInfoUpdateListener {
private static final int MSG_SET_SCRUBBING_MODE_ENABLED = 36;
private static final int MSG_SEEK_COMPLETED_IN_SCRUBBING_MODE = 37;
private static final int MSG_SET_SCRUBBING_MODE_PARAMETERS = 38;
+ private static final int MSG_AUDIO_PRESENTATIONS_CHANGED = 39;
private static final long BUFFERING_MAXIMUM_INTERVAL_MS =
Util.usToMs(Renderer.DEFAULT_DURATION_TO_PROGRESS_US);
@@ -256,7 +262,7 @@ public interface PlaybackInfoUpdateListener {
private long prewarmingMediaPeriodDiscontinuity = C.TIME_UNSET;
private boolean isPrewarmingDisabledUntilNextTransition;
private float volume;
-
+ private final AudioPresentationsChangeListener audioPresentationsChangeListener;
public ExoPlayerImplInternal(
Context context,
Renderer[] renderers,
@@ -279,7 +285,8 @@ public ExoPlayerImplInternal(
PlayerId playerId,
@Nullable PlaybackLooperProvider playbackLooperProvider,
PreloadConfiguration preloadConfiguration,
- VideoFrameMetadataListener videoFrameMetadataListener) {
+ VideoFrameMetadataListener videoFrameMetadataListener,
+ AudioPresentationsChangeListener audioPresentationsChangeListener) {
this.playbackInfoUpdateListener = playbackInfoUpdateListener;
this.trackSelector = trackSelector;
this.emptyTrackSelectorResult = emptyTrackSelectorResult;
@@ -299,6 +306,7 @@ public ExoPlayerImplInternal(
this.analyticsCollector = analyticsCollector;
this.volume = 1f;
this.scrubbingModeParameters = ScrubbingModeParameters.DEFAULT;
+ this.audioPresentationsChangeListener = audioPresentationsChangeListener;
playbackMaybeBecameStuckAtMs = C.TIME_UNSET;
lastRebufferRealtimeMs = C.TIME_UNSET;
@@ -615,6 +623,11 @@ public void onPrepared(MediaPeriod source) {
handler.obtainMessage(MSG_PERIOD_PREPARED, source).sendToTarget();
}
+ @Override
+ public void onAudioPresentationsChanged(List audioPresentations) {
+ handler.obtainMessage(MSG_AUDIO_PRESENTATIONS_CHANGED, audioPresentations).sendToTarget();
+ }
+
@Override
public void onContinueLoadingRequested(MediaPeriod source) {
handler.obtainMessage(MSG_SOURCE_CONTINUE_LOADING_REQUESTED, source).sendToTarget();
@@ -735,6 +748,9 @@ public boolean handleMessage(Message msg) {
case MSG_PERIOD_PREPARED:
handlePeriodPrepared((MediaPeriod) msg.obj);
break;
+ case MSG_AUDIO_PRESENTATIONS_CHANGED:
+ handleAudioPresentationsChanged((List) msg.obj);
+ break;
case MSG_SOURCE_CONTINUE_LOADING_REQUESTED:
handleContinueLoadingRequested((MediaPeriod) msg.obj);
break;
@@ -3008,6 +3024,10 @@ private void handleLoadingPeriodPrepared(MediaPeriodHolder loadingPeriodHolder)
maybeContinueLoading();
}
+ private void handleAudioPresentationsChanged(List audioPresentations) {
+ audioPresentationsChangeListener.onAudioPresentationsChanged(audioPresentations);
+ }
+
private void handleContinueLoadingRequested(MediaPeriod mediaPeriod) {
if (queue.isLoading(mediaPeriod)) {
queue.reevaluateBuffer(rendererPositionUs);
diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/MaskingMediaPeriod.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/MaskingMediaPeriod.java
index f2ba4bb1d3..d1705eb057 100644
--- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/MaskingMediaPeriod.java
+++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/MaskingMediaPeriod.java
@@ -19,6 +19,7 @@
import static androidx.media3.common.util.Assertions.checkState;
import static androidx.media3.common.util.Util.castNonNull;
+import android.media.AudioPresentation;
import androidx.annotation.Nullable;
import androidx.media3.common.C;
import androidx.media3.common.util.NullableType;
@@ -29,6 +30,7 @@
import androidx.media3.exoplayer.trackselection.ExoTrackSelection;
import androidx.media3.exoplayer.upstream.Allocator;
import java.io.IOException;
+import java.util.List;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
/**
@@ -249,6 +251,11 @@ public void onPrepared(MediaPeriod mediaPeriod) {
}
}
+ @Override
+ public void onAudioPresentationsChanged(List audioPresentations) {
+ castNonNull(callback).onAudioPresentationsChanged(audioPresentations);
+ }
+
private long getPreparePositionWithOverride(long preparePositionUs) {
return preparePositionOverrideUs != C.TIME_UNSET
? preparePositionOverrideUs
diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/MediaPeriod.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/MediaPeriod.java
index 2ab6a4764b..b1bf9e39c7 100644
--- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/MediaPeriod.java
+++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/MediaPeriod.java
@@ -15,6 +15,8 @@
*/
package androidx.media3.exoplayer.source;
+import android.media.AudioPresentation;
+import androidx.annotation.NonNull;
import androidx.media3.common.C;
import androidx.media3.common.StreamKey;
import androidx.media3.common.Timeline;
@@ -55,6 +57,14 @@ interface Callback extends SequenceableLoader.Callback {
* @param mediaPeriod The prepared {@link MediaPeriod}.
*/
void onPrepared(MediaPeriod mediaPeriod);
+
+ /**
+ * Called when the audio presentations, available to the player for the current track, are
+ * changed.
+ *
+ * @param audioPresentations The available audio presentations. Never null, but may be empty.
+ */
+ default void onAudioPresentationsChanged(@NonNull List audioPresentations) {}
}
/**
diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/ProgressiveMediaPeriod.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/ProgressiveMediaPeriod.java
index dfdd90bf44..43f7697761 100644
--- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/ProgressiveMediaPeriod.java
+++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/ProgressiveMediaPeriod.java
@@ -19,6 +19,7 @@
import static java.lang.Math.max;
import static java.lang.Math.min;
+import android.media.AudioPresentation;
import android.net.Uri;
import android.os.Handler;
import androidx.annotation.Nullable;
@@ -46,6 +47,7 @@
import androidx.media3.exoplayer.drm.DrmSessionEventListener;
import androidx.media3.exoplayer.drm.DrmSessionManager;
import androidx.media3.exoplayer.source.SampleQueue.UpstreamFormatChangedListener;
+import androidx.media3.exoplayer.source.SampleQueue.UpstreamAudioPresentationsChangedListener;
import androidx.media3.exoplayer.source.SampleStream.ReadFlags;
import androidx.media3.exoplayer.trackselection.ExoTrackSelection;
import androidx.media3.exoplayer.upstream.Allocator;
@@ -71,6 +73,7 @@
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
+import java.util.List;
import java.util.Map;
import org.checkerframework.checker.nullness.qual.EnsuresNonNull;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
@@ -81,7 +84,8 @@
ExtractorOutput,
Loader.Callback,
Loader.ReleaseCallback,
- UpstreamFormatChangedListener {
+ UpstreamFormatChangedListener,
+ UpstreamAudioPresentationsChangedListener {
/** Listener for information about the period. */
interface Listener {
@@ -792,6 +796,11 @@ public void onUpstreamFormatChanged(Format format) {
handler.post(maybeFinishPrepareRunnable);
}
+ @Override
+ public void onUpstreamAudioPresentationsChanged(List audioPresentations) {
+ handler.post(() -> updateAudioPresentations(audioPresentations));
+ }
+
// Internal methods.
private void onLengthKnown() {
@@ -812,6 +821,7 @@ private TrackOutput prepareTrackOutput(TrackId id) {
SampleQueue trackOutput =
SampleQueue.createWithDrm(allocator, drmSessionManager, drmEventDispatcher);
trackOutput.setUpstreamFormatChangeListener(this);
+ trackOutput.setUpstreamAudioPresentationsChangeListener(this);
@NullableType
TrackId[] sampleQueueTrackIds = Arrays.copyOf(this.sampleQueueTrackIds, trackCount + 1);
sampleQueueTrackIds[trackCount] = id;
@@ -822,6 +832,10 @@ private TrackOutput prepareTrackOutput(TrackId id) {
return trackOutput;
}
+ private void updateAudioPresentations(List audioPresentations) {
+ checkNotNull(callback).onAudioPresentationsChanged(audioPresentations);
+ }
+
private void setSeekMap(SeekMap seekMap) {
this.seekMap = icyHeaders == null ? seekMap : new Unseekable(/* durationUs= */ C.TIME_UNSET);
durationUs = seekMap.getDurationUs();
diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/SampleQueue.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/SampleQueue.java
index cb4ec71aab..b646aa19a6 100644
--- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/SampleQueue.java
+++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/SampleQueue.java
@@ -22,6 +22,7 @@
import static androidx.media3.exoplayer.source.SampleStream.FLAG_REQUIRE_FORMAT;
import static java.lang.Math.max;
+import android.media.AudioPresentation;
import android.os.Looper;
import androidx.annotation.CallSuper;
import androidx.annotation.GuardedBy;
@@ -50,6 +51,7 @@
import androidx.media3.exoplayer.upstream.Allocator;
import androidx.media3.extractor.TrackOutput;
import java.io.IOException;
+import java.util.List;
import java.util.Objects;
/** A queue of media samples. */
@@ -67,6 +69,17 @@ public interface UpstreamFormatChangedListener {
void onUpstreamFormatChanged(Format format);
}
+ /** A listener for changes to the upstream audio presentations. */
+ public interface UpstreamAudioPresentationsChangedListener {
+
+ /**
+ * Called on the loading thread when an upstream audio presentation change occurs.
+ *
+ * @param audioPresentations The new upstream audio presentations.
+ */
+ void onUpstreamAudioPresentationsChanged(List audioPresentations);
+ }
+
@VisibleForTesting /* package */ static final int SAMPLE_CAPACITY_INCREMENT = 1000;
private static final String TAG = "SampleQueue";
@@ -76,6 +89,8 @@ public interface UpstreamFormatChangedListener {
@Nullable private final DrmSessionManager drmSessionManager;
@Nullable private final DrmSessionEventListener.EventDispatcher drmEventDispatcher;
@Nullable private UpstreamFormatChangedListener upstreamFormatChangeListener;
+ @Nullable private UpstreamAudioPresentationsChangedListener
+ upstreamAudioPresentationsChangeListener;
@Nullable private Format downstreamFormat;
@Nullable private DrmSession currentDrmSession;
@@ -582,6 +597,16 @@ public final void setUpstreamFormatChangeListener(
upstreamFormatChangeListener = listener;
}
+ /**
+ * Sets a listener to be notified of changes to the upstream audio presentations.
+ *
+ * @param listener The listener.
+ */
+ public final void setUpstreamAudioPresentationsChangeListener(
+ @Nullable UpstreamAudioPresentationsChangedListener listener) {
+ upstreamAudioPresentationsChangeListener = listener;
+ }
+
// TrackOutput implementation. Called by the loading thread.
@Override
@@ -589,10 +614,21 @@ public final void format(Format format) {
Format adjustedUpstreamFormat = getAdjustedUpstreamFormat(format);
upstreamFormatAdjustmentRequired = false;
unadjustedUpstreamFormat = format;
+ boolean audioPresentationsChanged = false;
+ if (format.sampleMimeType != null &&
+ MimeTypes.getTrackType(format.sampleMimeType) == C.TRACK_TYPE_AUDIO) {
+ audioPresentationsChanged =
+ upstreamFormat == null ? !format.audioPresentations.isEmpty() :
+ !format.audioPresentations.equals(upstreamFormat.audioPresentations);
+ }
boolean upstreamFormatChanged = setUpstreamFormat(adjustedUpstreamFormat);
if (upstreamFormatChangeListener != null && upstreamFormatChanged) {
upstreamFormatChangeListener.onUpstreamFormatChanged(adjustedUpstreamFormat);
}
+ if (upstreamAudioPresentationsChangeListener != null && audioPresentationsChanged) {
+ upstreamAudioPresentationsChangeListener.onUpstreamAudioPresentationsChanged(
+ upstreamFormat.audioPresentations);
+ }
}
@Override
diff --git a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/ExoPlayerTest.java b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/ExoPlayerTest.java
index cccf8c61df..d088cafc6a 100644
--- a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/ExoPlayerTest.java
+++ b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/ExoPlayerTest.java
@@ -100,15 +100,19 @@
import android.content.Intent;
import android.content.pm.PackageManager;
import android.graphics.SurfaceTexture;
+import android.icu.util.ULocale;
import android.media.AudioDeviceInfo;
import android.media.AudioManager;
+import android.media.AudioPresentation;
import android.media.AudioTrack;
import android.net.Uri;
+import android.os.Build;
import android.os.Handler;
import android.os.Looper;
import android.util.Pair;
import android.view.Surface;
import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
import androidx.media3.common.AdPlaybackState;
import androidx.media3.common.AudioAttributes;
import androidx.media3.common.C;
@@ -259,6 +263,8 @@ public final class ExoPlayerTest {
private static final int TIMEOUT_MS = 10_000;
private static final String SAMPLE_URI = "asset://android_asset/media/mp4/sample.mp4";
+ private static final String SAMPLE_AC4_MP4_URI =
+ "asset://android_asset/media/mp4/sample_ac4_multiple_presentations.mp4";
@Parameters(name = "preload={0}")
public static ImmutableList