Add FilteringMediaSource that only provides tracks of given types

This is useful for cases where only certain types (e.g. only video)
from a source are needed and other tracks should be filtered out
completely to avoid later track selection issues.

#minor-release

PiperOrigin-RevId: 533394658
This commit is contained in:
tonihei 2023-05-19 10:14:13 +01:00 committed by Ian Baker
parent 25fa2df2de
commit c44b3828ca
3 changed files with 279 additions and 0 deletions

View File

@ -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

View File

@ -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}.
*
* <p>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<StreamKey> getStreamKeys(List<ExoTrackSelection> 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<TrackGroup> 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);
}
}
}

View File

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