diff --git a/RELEASENOTES.md b/RELEASENOTES.md
index c3c3677ae3..565207b37a 100644
--- a/RELEASENOTES.md
+++ b/RELEASENOTES.md
@@ -10,6 +10,9 @@
* Enable multi-period live DASH streams for DAI. Please note that the
current implementation does not yet support seeking in live streams
([#10912](https://github.com/google/ExoPlayer/issues/10912)).
+* ExoPlayer:
+ * Add `FilteringMediaSource` that allows to filter available track types
+ from a `MediaSource`.
* Session:
* Add `androidx.media3.session.MediaButtonReceiver` to enable apps to
implement playback resumption with media button events sent by, for
diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/FilteringMediaSource.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/FilteringMediaSource.java
new file mode 100644
index 0000000000..2e7b8b185e
--- /dev/null
+++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/source/FilteringMediaSource.java
@@ -0,0 +1,192 @@
+/*
+ * Copyright 2023 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.source;
+
+import static androidx.media3.common.util.Assertions.checkNotNull;
+
+import androidx.annotation.Nullable;
+import androidx.media3.common.C;
+import androidx.media3.common.StreamKey;
+import androidx.media3.common.TrackGroup;
+import androidx.media3.common.util.UnstableApi;
+import androidx.media3.exoplayer.SeekParameters;
+import androidx.media3.exoplayer.trackselection.ExoTrackSelection;
+import androidx.media3.exoplayer.upstream.Allocator;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import java.io.IOException;
+import java.util.List;
+import java.util.Set;
+import org.checkerframework.checker.nullness.compatqual.NullableType;
+
+/**
+ * A {@link MediaSource} that filters the available {@linkplain C.TrackType track types}.
+ *
+ *
Media sources loading muxed media, e.g. progressive streams with muxed video and audio, are
+ * still likely to parse all of these streams even if the tracks are not made available to the
+ * player.
+ */
+@UnstableApi
+public class FilteringMediaSource extends WrappingMediaSource {
+
+ private final ImmutableSet<@C.TrackType Integer> trackTypes;
+
+ /**
+ * Creates a filtering {@link MediaSource} that only publishes tracks of one type.
+ *
+ * @param mediaSource The wrapped {@link MediaSource}.
+ * @param trackType The only {@link C.TrackType} to provide from this source.
+ */
+ public FilteringMediaSource(MediaSource mediaSource, @C.TrackType int trackType) {
+ this(mediaSource, ImmutableSet.of(trackType));
+ }
+
+ /**
+ * Creates a filtering {@link MediaSource} that only publishes tracks of the given types.
+ *
+ * @param mediaSource The wrapped {@link MediaSource}.
+ * @param trackTypes The {@linkplain C.TrackType track types} to provide from this source.
+ */
+ public FilteringMediaSource(MediaSource mediaSource, Set<@C.TrackType Integer> trackTypes) {
+ super(mediaSource);
+ this.trackTypes = ImmutableSet.copyOf(trackTypes);
+ }
+
+ @Override
+ public MediaPeriod createPeriod(MediaPeriodId id, Allocator allocator, long startPositionUs) {
+ MediaPeriod wrappedPeriod = super.createPeriod(id, allocator, startPositionUs);
+ return new FilteringMediaPeriod(wrappedPeriod, trackTypes);
+ }
+
+ @Override
+ public void releasePeriod(MediaPeriod mediaPeriod) {
+ MediaPeriod wrappedPeriod = ((FilteringMediaPeriod) mediaPeriod).mediaPeriod;
+ super.releasePeriod(wrappedPeriod);
+ }
+
+ private static final class FilteringMediaPeriod implements MediaPeriod, MediaPeriod.Callback {
+
+ public final MediaPeriod mediaPeriod;
+
+ private final ImmutableSet<@C.TrackType Integer> trackTypes;
+
+ @Nullable private Callback callback;
+ @Nullable private TrackGroupArray filteredTrackGroups;
+
+ public FilteringMediaPeriod(
+ MediaPeriod mediaPeriod, ImmutableSet<@C.TrackType Integer> trackTypes) {
+ this.mediaPeriod = mediaPeriod;
+ this.trackTypes = trackTypes;
+ }
+
+ @Override
+ public void prepare(Callback callback, long positionUs) {
+ this.callback = callback;
+ mediaPeriod.prepare(/* callback= */ this, positionUs);
+ }
+
+ @Override
+ public void maybeThrowPrepareError() throws IOException {
+ mediaPeriod.maybeThrowPrepareError();
+ }
+
+ @Override
+ public TrackGroupArray getTrackGroups() {
+ return checkNotNull(filteredTrackGroups);
+ }
+
+ @Override
+ public List getStreamKeys(List trackSelections) {
+ return mediaPeriod.getStreamKeys(trackSelections);
+ }
+
+ @Override
+ public long selectTracks(
+ @NullableType ExoTrackSelection[] selections,
+ boolean[] mayRetainStreamFlags,
+ @NullableType SampleStream[] streams,
+ boolean[] streamResetFlags,
+ long positionUs) {
+ return mediaPeriod.selectTracks(
+ selections, mayRetainStreamFlags, streams, streamResetFlags, positionUs);
+ }
+
+ @Override
+ public void discardBuffer(long positionUs, boolean toKeyframe) {
+ mediaPeriod.discardBuffer(positionUs, toKeyframe);
+ }
+
+ @Override
+ public long readDiscontinuity() {
+ return mediaPeriod.readDiscontinuity();
+ }
+
+ @Override
+ public long seekToUs(long positionUs) {
+ return mediaPeriod.seekToUs(positionUs);
+ }
+
+ @Override
+ public long getAdjustedSeekPositionUs(long positionUs, SeekParameters seekParameters) {
+ return mediaPeriod.getAdjustedSeekPositionUs(positionUs, seekParameters);
+ }
+
+ @Override
+ public long getBufferedPositionUs() {
+ return mediaPeriod.getBufferedPositionUs();
+ }
+
+ @Override
+ public long getNextLoadPositionUs() {
+ return mediaPeriod.getNextLoadPositionUs();
+ }
+
+ @Override
+ public boolean continueLoading(long positionUs) {
+ return mediaPeriod.continueLoading(positionUs);
+ }
+
+ @Override
+ public boolean isLoading() {
+ return mediaPeriod.isLoading();
+ }
+
+ @Override
+ public void reevaluateBuffer(long positionUs) {
+ mediaPeriod.reevaluateBuffer(positionUs);
+ }
+
+ @Override
+ public void onPrepared(MediaPeriod mediaPeriod) {
+ TrackGroupArray trackGroups = mediaPeriod.getTrackGroups();
+ ImmutableList.Builder trackGroupsBuilder = ImmutableList.builder();
+ for (int i = 0; i < trackGroups.length; i++) {
+ TrackGroup trackGroup = trackGroups.get(i);
+ if (trackTypes.contains(trackGroup.type)) {
+ trackGroupsBuilder.add(trackGroup);
+ }
+ }
+ filteredTrackGroups =
+ new TrackGroupArray(trackGroupsBuilder.build().toArray(new TrackGroup[0]));
+ checkNotNull(callback).onPrepared(/* mediaPeriod= */ this);
+ }
+
+ @Override
+ public void onContinueLoadingRequested(MediaPeriod source) {
+ checkNotNull(callback).onContinueLoadingRequested(/* source= */ this);
+ }
+ }
+}
diff --git a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/source/FilteringMediaSourceTest.java b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/source/FilteringMediaSourceTest.java
new file mode 100644
index 0000000000..8c054ac125
--- /dev/null
+++ b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/source/FilteringMediaSourceTest.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright 2023 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.source;
+
+import static androidx.media3.test.utils.robolectric.TestPlayerRunHelper.runUntilPlaybackState;
+import static com.google.common.truth.Truth.assertThat;
+
+import androidx.media3.common.C;
+import androidx.media3.common.Format;
+import androidx.media3.common.MimeTypes;
+import androidx.media3.common.Player;
+import androidx.media3.common.Timeline;
+import androidx.media3.common.Tracks;
+import androidx.media3.exoplayer.ExoPlayer;
+import androidx.media3.test.utils.FakeMediaSource;
+import androidx.media3.test.utils.FakeRenderer;
+import androidx.media3.test.utils.FakeTimeline;
+import androidx.media3.test.utils.TestExoPlayerBuilder;
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import com.google.common.collect.ImmutableSet;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/** Unit tests for {@link FilteringMediaSource}. */
+@RunWith(AndroidJUnit4.class)
+public class FilteringMediaSourceTest {
+
+ @Test
+ public void playbackWithFilteredMediaSource_onlyPublishesAndPlaysAllowedTypes() throws Exception {
+ Timeline timeline = new FakeTimeline();
+ FakeMediaSource videoSource =
+ new FakeMediaSource(
+ timeline, new Format.Builder().setSampleMimeType(MimeTypes.VIDEO_H264).build());
+ FakeMediaSource audioSource =
+ new FakeMediaSource(
+ timeline, new Format.Builder().setSampleMimeType(MimeTypes.AUDIO_AAC).build());
+ FakeMediaSource textSource =
+ new FakeMediaSource(
+ timeline,
+ new Format.Builder()
+ .setSampleMimeType(MimeTypes.TEXT_VTT)
+ .setSelectionFlags(C.SELECTION_FLAG_FORCED)
+ .build());
+ FakeRenderer videoRenderer = new FakeRenderer(C.TRACK_TYPE_VIDEO);
+ FakeRenderer audioRenderer = new FakeRenderer(C.TRACK_TYPE_AUDIO);
+ FakeRenderer textRenderer = new FakeRenderer(C.TRACK_TYPE_TEXT);
+ ExoPlayer player =
+ new TestExoPlayerBuilder(ApplicationProvider.getApplicationContext())
+ .setRenderers(videoRenderer, audioRenderer, textRenderer)
+ .build();
+ FilteringMediaSource mediaSourceWithVideoAndTextOnly =
+ new FilteringMediaSource(
+ new MergingMediaSource(textSource, audioSource, videoSource),
+ ImmutableSet.of(C.TRACK_TYPE_VIDEO, C.TRACK_TYPE_TEXT));
+ player.setMediaSource(mediaSourceWithVideoAndTextOnly);
+
+ player.prepare();
+ runUntilPlaybackState(player, Player.STATE_READY);
+ Tracks tracks = player.getCurrentTracks();
+ player.play();
+ runUntilPlaybackState(player, Player.STATE_ENDED);
+ player.release();
+
+ assertThat(tracks.getGroups()).hasSize(2);
+ assertThat(tracks.containsType(C.TRACK_TYPE_AUDIO)).isFalse();
+ assertThat(videoRenderer.enabledCount).isEqualTo(1);
+ assertThat(textRenderer.enabledCount).isEqualTo(1);
+ assertThat(audioRenderer.enabledCount).isEqualTo(0);
+ }
+}