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:
parent
25fa2df2de
commit
c44b3828ca
@ -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
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user