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); + } +}