Add HlsMediaPeriod getStreamKeys implementation and tests.
PiperOrigin-RevId: 231385563
This commit is contained in:
parent
6a52cd7445
commit
32b40502fc
@ -20,6 +20,7 @@ import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.Format;
|
||||
import com.google.android.exoplayer2.SeekParameters;
|
||||
import com.google.android.exoplayer2.extractor.Extractor;
|
||||
import com.google.android.exoplayer2.offline.StreamKey;
|
||||
import com.google.android.exoplayer2.source.CompositeSequenceableLoaderFactory;
|
||||
import com.google.android.exoplayer2.source.MediaPeriod;
|
||||
import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher;
|
||||
@ -68,6 +69,7 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper
|
||||
private TrackGroupArray trackGroups;
|
||||
private HlsSampleStreamWrapper[] sampleStreamWrappers;
|
||||
private HlsSampleStreamWrapper[] enabledSampleStreamWrappers;
|
||||
private int[] selectedVariantIndices;
|
||||
private SequenceableLoader compositeSequenceableLoader;
|
||||
private boolean notifiedReadingStarted;
|
||||
|
||||
@ -112,6 +114,7 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper
|
||||
timestampAdjusterProvider = new TimestampAdjusterProvider();
|
||||
sampleStreamWrappers = new HlsSampleStreamWrapper[0];
|
||||
enabledSampleStreamWrappers = new HlsSampleStreamWrapper[0];
|
||||
selectedVariantIndices = new int[0];
|
||||
eventDispatcher.mediaPeriodCreated();
|
||||
}
|
||||
|
||||
@ -143,6 +146,77 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper
|
||||
return trackGroups;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<StreamKey> getStreamKeys(List<TrackSelection> trackSelections) {
|
||||
// See HlsMasterPlaylist.copy for interpretation of StreamKeys.
|
||||
HlsMasterPlaylist masterPlaylist = Assertions.checkNotNull(playlistTracker.getMasterPlaylist());
|
||||
boolean hasVariants = !masterPlaylist.variants.isEmpty();
|
||||
int audioWrapperOffset = hasVariants ? 1 : 0;
|
||||
int subtitleWrapperOffset = audioWrapperOffset + masterPlaylist.audios.size();
|
||||
|
||||
TrackGroupArray mainWrapperTrackGroups;
|
||||
int mainWrapperPrimaryGroupIndex;
|
||||
if (hasVariants) {
|
||||
HlsSampleStreamWrapper mainWrapper = sampleStreamWrappers[0];
|
||||
mainWrapperTrackGroups = mainWrapper.getTrackGroups();
|
||||
mainWrapperPrimaryGroupIndex = mainWrapper.getPrimaryTrackGroupIndex();
|
||||
} else {
|
||||
mainWrapperTrackGroups = TrackGroupArray.EMPTY;
|
||||
mainWrapperPrimaryGroupIndex = 0;
|
||||
}
|
||||
|
||||
List<StreamKey> streamKeys = new ArrayList<>();
|
||||
boolean needsPrimaryTrackGroupSelection = false;
|
||||
boolean hasPrimaryTrackGroupSelection = false;
|
||||
for (TrackSelection trackSelection : trackSelections) {
|
||||
TrackGroup trackSelectionGroup = trackSelection.getTrackGroup();
|
||||
int mainWrapperTrackGroupIndex = mainWrapperTrackGroups.indexOf(trackSelectionGroup);
|
||||
if (mainWrapperTrackGroupIndex != C.INDEX_UNSET) {
|
||||
if (mainWrapperTrackGroupIndex == mainWrapperPrimaryGroupIndex) {
|
||||
// Primary group in main wrapper.
|
||||
hasPrimaryTrackGroupSelection = true;
|
||||
for (int i = 0; i < trackSelection.length(); i++) {
|
||||
int variantIndex = selectedVariantIndices[trackSelection.getIndexInTrackGroup(i)];
|
||||
streamKeys.add(new StreamKey(HlsMasterPlaylist.GROUP_INDEX_VARIANT, variantIndex));
|
||||
}
|
||||
} else {
|
||||
// Embedded group in main wrapper.
|
||||
needsPrimaryTrackGroupSelection = true;
|
||||
}
|
||||
} else {
|
||||
// Audio or subtitle group.
|
||||
for (int i = audioWrapperOffset; i < sampleStreamWrappers.length; i++) {
|
||||
TrackGroupArray wrapperTrackGroups = sampleStreamWrappers[i].getTrackGroups();
|
||||
if (wrapperTrackGroups.indexOf(trackSelectionGroup) != C.INDEX_UNSET) {
|
||||
if (i < subtitleWrapperOffset) {
|
||||
streamKeys.add(
|
||||
new StreamKey(HlsMasterPlaylist.GROUP_INDEX_AUDIO, i - audioWrapperOffset));
|
||||
} else {
|
||||
streamKeys.add(
|
||||
new StreamKey(HlsMasterPlaylist.GROUP_INDEX_SUBTITLE, i - subtitleWrapperOffset));
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (needsPrimaryTrackGroupSelection && !hasPrimaryTrackGroupSelection) {
|
||||
// A track selection includes a variant-embedded track, but no variant is added yet. We use
|
||||
// the valid variant with the lowest bitrate to reduce overhead.
|
||||
int lowestBitrateIndex = selectedVariantIndices[0];
|
||||
int lowestBitrate = masterPlaylist.variants.get(selectedVariantIndices[0]).format.bitrate;
|
||||
for (int i = 1; i < selectedVariantIndices.length; i++) {
|
||||
int variantBitrate = masterPlaylist.variants.get(selectedVariantIndices[i]).format.bitrate;
|
||||
if (variantBitrate < lowestBitrate) {
|
||||
lowestBitrate = variantBitrate;
|
||||
lowestBitrateIndex = selectedVariantIndices[i];
|
||||
}
|
||||
}
|
||||
streamKeys.add(new StreamKey(HlsMasterPlaylist.GROUP_INDEX_VARIANT, lowestBitrateIndex));
|
||||
}
|
||||
return streamKeys;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long selectTracks(TrackSelection[] selections, boolean[] mayRetainStreamFlags,
|
||||
SampleStream[] streams, boolean[] streamResetFlags, long positionUs) {
|
||||
@ -424,44 +498,64 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper
|
||||
*/
|
||||
private void buildAndPrepareMainSampleStreamWrapper(
|
||||
HlsMasterPlaylist masterPlaylist, long positionUs) {
|
||||
List<HlsUrl> selectedVariants = new ArrayList<>(masterPlaylist.variants);
|
||||
ArrayList<HlsUrl> definiteVideoVariants = new ArrayList<>();
|
||||
ArrayList<HlsUrl> definiteAudioOnlyVariants = new ArrayList<>();
|
||||
for (int i = 0; i < selectedVariants.size(); i++) {
|
||||
HlsUrl variant = selectedVariants.get(i);
|
||||
int[] variantTypes = new int[masterPlaylist.variants.size()];
|
||||
int videoVariantCount = 0;
|
||||
int audioVariantCount = 0;
|
||||
for (int i = 0; i < masterPlaylist.variants.size(); i++) {
|
||||
HlsUrl variant = masterPlaylist.variants.get(i);
|
||||
Format format = variant.format;
|
||||
if (format.height > 0 || Util.getCodecsOfType(format.codecs, C.TRACK_TYPE_VIDEO) != null) {
|
||||
definiteVideoVariants.add(variant);
|
||||
variantTypes[i] = C.TRACK_TYPE_VIDEO;
|
||||
videoVariantCount++;
|
||||
} else if (Util.getCodecsOfType(format.codecs, C.TRACK_TYPE_AUDIO) != null) {
|
||||
definiteAudioOnlyVariants.add(variant);
|
||||
variantTypes[i] = C.TRACK_TYPE_AUDIO;
|
||||
audioVariantCount++;
|
||||
} else {
|
||||
variantTypes[i] = C.TRACK_TYPE_UNKNOWN;
|
||||
}
|
||||
}
|
||||
if (!definiteVideoVariants.isEmpty()) {
|
||||
boolean useVideoVariantsOnly = false;
|
||||
boolean useNonAudioVariantsOnly = false;
|
||||
int selectedVariantsCount = variantTypes.length;
|
||||
if (videoVariantCount > 0) {
|
||||
// We've identified some variants as definitely containing video. Assume variants within the
|
||||
// master playlist are marked consistently, and hence that we have the full set. Filter out
|
||||
// any other variants, which are likely to be audio only.
|
||||
selectedVariants = definiteVideoVariants;
|
||||
} else if (definiteAudioOnlyVariants.size() < selectedVariants.size()) {
|
||||
useVideoVariantsOnly = true;
|
||||
selectedVariantsCount = videoVariantCount;
|
||||
} else if (audioVariantCount < variantTypes.length) {
|
||||
// We've identified some variants, but not all, as being audio only. Filter them out to leave
|
||||
// the remaining variants, which are likely to contain video.
|
||||
selectedVariants.removeAll(definiteAudioOnlyVariants);
|
||||
} else {
|
||||
// Leave the enabled variants unchanged. They're likely either all video or all audio.
|
||||
useNonAudioVariantsOnly = true;
|
||||
selectedVariantsCount = variantTypes.length - audioVariantCount;
|
||||
}
|
||||
Assertions.checkArgument(!selectedVariants.isEmpty());
|
||||
HlsUrl[] variants = selectedVariants.toArray(new HlsUrl[0]);
|
||||
String codecs = variants[0].format.codecs;
|
||||
HlsSampleStreamWrapper sampleStreamWrapper = buildSampleStreamWrapper(C.TRACK_TYPE_DEFAULT,
|
||||
variants, masterPlaylist.muxedAudioFormat, masterPlaylist.muxedCaptionFormats, positionUs);
|
||||
HlsUrl[] selectedVariants = new HlsUrl[selectedVariantsCount];
|
||||
selectedVariantIndices = new int[selectedVariantsCount];
|
||||
int outIndex = 0;
|
||||
for (int i = 0; i < masterPlaylist.variants.size(); i++) {
|
||||
if ((!useVideoVariantsOnly || variantTypes[i] == C.TRACK_TYPE_VIDEO)
|
||||
&& (!useNonAudioVariantsOnly || variantTypes[i] != C.TRACK_TYPE_AUDIO)) {
|
||||
selectedVariants[outIndex] = masterPlaylist.variants.get(i);
|
||||
selectedVariantIndices[outIndex++] = i;
|
||||
}
|
||||
}
|
||||
String codecs = selectedVariants[0].format.codecs;
|
||||
HlsSampleStreamWrapper sampleStreamWrapper =
|
||||
buildSampleStreamWrapper(
|
||||
C.TRACK_TYPE_DEFAULT,
|
||||
selectedVariants,
|
||||
masterPlaylist.muxedAudioFormat,
|
||||
masterPlaylist.muxedCaptionFormats,
|
||||
positionUs);
|
||||
sampleStreamWrappers[0] = sampleStreamWrapper;
|
||||
if (allowChunklessPreparation && codecs != null) {
|
||||
boolean variantsContainVideoCodecs = Util.getCodecsOfType(codecs, C.TRACK_TYPE_VIDEO) != null;
|
||||
boolean variantsContainAudioCodecs = Util.getCodecsOfType(codecs, C.TRACK_TYPE_AUDIO) != null;
|
||||
List<TrackGroup> muxedTrackGroups = new ArrayList<>();
|
||||
if (variantsContainVideoCodecs) {
|
||||
Format[] videoFormats = new Format[selectedVariants.size()];
|
||||
Format[] videoFormats = new Format[selectedVariantsCount];
|
||||
for (int i = 0; i < videoFormats.length; i++) {
|
||||
videoFormats[i] = deriveVideoFormat(variants[i].format);
|
||||
videoFormats[i] = deriveVideoFormat(selectedVariants[i].format);
|
||||
}
|
||||
muxedTrackGroups.add(new TrackGroup(videoFormats));
|
||||
|
||||
@ -470,7 +564,7 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper
|
||||
muxedTrackGroups.add(
|
||||
new TrackGroup(
|
||||
deriveAudioFormat(
|
||||
variants[0].format,
|
||||
selectedVariants[0].format,
|
||||
masterPlaylist.muxedAudioFormat,
|
||||
/* isPrimaryTrackInVariant= */ false)));
|
||||
}
|
||||
@ -482,9 +576,9 @@ public final class HlsMediaPeriod implements MediaPeriod, HlsSampleStreamWrapper
|
||||
}
|
||||
} else if (variantsContainAudioCodecs) {
|
||||
// Variants only contain audio.
|
||||
Format[] audioFormats = new Format[selectedVariants.size()];
|
||||
Format[] audioFormats = new Format[selectedVariantsCount];
|
||||
for (int i = 0; i < audioFormats.length; i++) {
|
||||
Format variantFormat = variants[i].format;
|
||||
Format variantFormat = selectedVariants[i].format;
|
||||
audioFormats[i] =
|
||||
deriveAudioFormat(
|
||||
variantFormat,
|
||||
|
@ -214,6 +214,10 @@ import java.util.List;
|
||||
return trackGroups;
|
||||
}
|
||||
|
||||
public int getPrimaryTrackGroupIndex() {
|
||||
return primaryTrackGroupIndex;
|
||||
}
|
||||
|
||||
public int bindSampleQueueToSampleStream(int trackGroupIndex) {
|
||||
int sampleQueueIndex = trackGroupToSampleQueueIndex[trackGroupIndex];
|
||||
if (sampleQueueIndex == C.INDEX_UNSET) {
|
||||
|
@ -0,0 +1,184 @@
|
||||
/*
|
||||
* Copyright (C) 2018 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 com.google.android.exoplayer2.source.hls;
|
||||
|
||||
import static org.mockito.Matchers.anyInt;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import com.google.android.exoplayer2.Format;
|
||||
import com.google.android.exoplayer2.source.CompositeSequenceableLoaderFactory;
|
||||
import com.google.android.exoplayer2.source.MediaSource.MediaPeriodId;
|
||||
import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher;
|
||||
import com.google.android.exoplayer2.source.hls.playlist.HlsMasterPlaylist;
|
||||
import com.google.android.exoplayer2.source.hls.playlist.HlsMasterPlaylist.HlsUrl;
|
||||
import com.google.android.exoplayer2.source.hls.playlist.HlsPlaylist;
|
||||
import com.google.android.exoplayer2.source.hls.playlist.HlsPlaylistTracker;
|
||||
import com.google.android.exoplayer2.testutil.MediaPeriodAsserts;
|
||||
import com.google.android.exoplayer2.testutil.MediaPeriodAsserts.FilterableManifestMediaPeriodFactory;
|
||||
import com.google.android.exoplayer2.testutil.RobolectricUtil;
|
||||
import com.google.android.exoplayer2.upstream.Allocator;
|
||||
import com.google.android.exoplayer2.upstream.DataSource;
|
||||
import com.google.android.exoplayer2.upstream.LoadErrorHandlingPolicy;
|
||||
import com.google.android.exoplayer2.upstream.TransferListener;
|
||||
import com.google.android.exoplayer2.util.MimeTypes;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.robolectric.RobolectricTestRunner;
|
||||
import org.robolectric.annotation.Config;
|
||||
|
||||
/** Unit test for {@link HlsMediaPeriod}. */
|
||||
@RunWith(RobolectricTestRunner.class)
|
||||
@Config(shadows = {RobolectricUtil.CustomLooper.class, RobolectricUtil.CustomMessageQueue.class})
|
||||
public final class HlsMediaPeriodTest {
|
||||
|
||||
@Test
|
||||
public void getSteamKeys_isCompatibleWithhHlsMasterPlaylistFilter() {
|
||||
HlsMasterPlaylist testMasterPlaylist =
|
||||
createMasterPlaylist(
|
||||
/* variants= */ Arrays.asList(
|
||||
createAudioOnlyVariantHlsUrl(/* bitrate= */ 10000),
|
||||
createMuxedVideoAudioVariantHlsUrl(/* bitrate= */ 200000),
|
||||
createAudioOnlyVariantHlsUrl(/* bitrate= */ 300000),
|
||||
createMuxedVideoAudioVariantHlsUrl(/* bitrate= */ 400000),
|
||||
createMuxedVideoAudioVariantHlsUrl(/* bitrate= */ 600000)),
|
||||
/* audios= */ Arrays.asList(
|
||||
createAudioHlsUrl(/* language= */ "spa"),
|
||||
createAudioHlsUrl(/* language= */ "ger"),
|
||||
createAudioHlsUrl(/* language= */ "tur")),
|
||||
/* subtitles= */ Arrays.asList(
|
||||
createSubtitleHlsUrl(/* language= */ "spa"),
|
||||
createSubtitleHlsUrl(/* language= */ "ger"),
|
||||
createSubtitleHlsUrl(/* language= */ "tur")),
|
||||
/* muxedAudioFormat= */ createAudioFormat("eng"),
|
||||
/* muxedCaptionFormats= */ Arrays.asList(
|
||||
createSubtitleFormat("eng"), createSubtitleFormat("gsw")));
|
||||
FilterableManifestMediaPeriodFactory<HlsPlaylist> mediaPeriodFactory =
|
||||
(playlist, periodIndex) -> {
|
||||
HlsDataSourceFactory mockDataSourceFactory = mock(HlsDataSourceFactory.class);
|
||||
when(mockDataSourceFactory.createDataSource(anyInt())).thenReturn(mock(DataSource.class));
|
||||
HlsPlaylistTracker mockPlaylistTracker = mock(HlsPlaylistTracker.class);
|
||||
when(mockPlaylistTracker.getMasterPlaylist()).thenReturn((HlsMasterPlaylist) playlist);
|
||||
return new HlsMediaPeriod(
|
||||
mock(HlsExtractorFactory.class),
|
||||
mockPlaylistTracker,
|
||||
mockDataSourceFactory,
|
||||
mock(TransferListener.class),
|
||||
mock(LoadErrorHandlingPolicy.class),
|
||||
new EventDispatcher()
|
||||
.withParameters(
|
||||
/* windowIndex= */ 0,
|
||||
/* mediaPeriodId= */ new MediaPeriodId(/* periodUid= */ new Object()),
|
||||
/* mediaTimeOffsetMs= */ 0),
|
||||
mock(Allocator.class),
|
||||
mock(CompositeSequenceableLoaderFactory.class),
|
||||
/* allowChunklessPreparation =*/ true);
|
||||
};
|
||||
|
||||
MediaPeriodAsserts.assertGetStreamKeysAndManifestFilterIntegration(
|
||||
mediaPeriodFactory, testMasterPlaylist);
|
||||
}
|
||||
|
||||
private static HlsMasterPlaylist createMasterPlaylist(
|
||||
List<HlsUrl> variants,
|
||||
List<HlsUrl> audios,
|
||||
List<HlsUrl> subtitles,
|
||||
Format muxedAudioFormat,
|
||||
List<Format> muxedCaptionFormats) {
|
||||
return new HlsMasterPlaylist(
|
||||
"http://baseUri",
|
||||
/* tags= */ Collections.emptyList(),
|
||||
variants,
|
||||
audios,
|
||||
subtitles,
|
||||
muxedAudioFormat,
|
||||
muxedCaptionFormats,
|
||||
/* hasIndependentSegments= */ true,
|
||||
/* variableDefinitions= */ Collections.emptyMap());
|
||||
}
|
||||
|
||||
private static HlsUrl createMuxedVideoAudioVariantHlsUrl(int bitrate) {
|
||||
return new HlsUrl(
|
||||
"http://url",
|
||||
Format.createVideoContainerFormat(
|
||||
/* id= */ null,
|
||||
/* label= */ null,
|
||||
/* containerMimeType= */ MimeTypes.APPLICATION_M3U8,
|
||||
/* sampleMimeType= */ null,
|
||||
/* codecs= */ "avc1.100.41,mp4a.40.2",
|
||||
bitrate,
|
||||
/* width= */ Format.NO_VALUE,
|
||||
/* height= */ Format.NO_VALUE,
|
||||
/* frameRate= */ Format.NO_VALUE,
|
||||
/* initializationData= */ null,
|
||||
/* selectionFlags= */ 0));
|
||||
}
|
||||
|
||||
private static HlsUrl createAudioOnlyVariantHlsUrl(int bitrate) {
|
||||
return new HlsUrl(
|
||||
"http://url",
|
||||
Format.createVideoContainerFormat(
|
||||
/* id= */ null,
|
||||
/* label= */ null,
|
||||
/* containerMimeType= */ MimeTypes.APPLICATION_M3U8,
|
||||
/* sampleMimeType= */ null,
|
||||
/* codecs= */ "mp4a.40.2",
|
||||
bitrate,
|
||||
/* width= */ Format.NO_VALUE,
|
||||
/* height= */ Format.NO_VALUE,
|
||||
/* frameRate= */ Format.NO_VALUE,
|
||||
/* initializationData= */ null,
|
||||
/* selectionFlags= */ 0));
|
||||
}
|
||||
|
||||
private static HlsUrl createAudioHlsUrl(String language) {
|
||||
return new HlsUrl("http://url", createAudioFormat(language));
|
||||
}
|
||||
|
||||
private static HlsUrl createSubtitleHlsUrl(String language) {
|
||||
return new HlsUrl("http://url", createSubtitleFormat(language));
|
||||
}
|
||||
|
||||
private static Format createAudioFormat(String language) {
|
||||
return Format.createAudioContainerFormat(
|
||||
/* id= */ null,
|
||||
/* label= */ null,
|
||||
/* containerMimeType= */ MimeTypes.APPLICATION_M3U8,
|
||||
MimeTypes.getMediaMimeType("mp4a.40.2"),
|
||||
/* codecs= */ "mp4a.40.2",
|
||||
/* bitrate= */ Format.NO_VALUE,
|
||||
/* channelCount= */ Format.NO_VALUE,
|
||||
/* sampleRate= */ Format.NO_VALUE,
|
||||
/* initializationData= */ null,
|
||||
/* selectionFlags= */ 0,
|
||||
language);
|
||||
}
|
||||
|
||||
private static Format createSubtitleFormat(String language) {
|
||||
return Format.createTextContainerFormat(
|
||||
/* id= */ null,
|
||||
/* label= */ null,
|
||||
/* containerMimeType= */ MimeTypes.APPLICATION_M3U8,
|
||||
/* sampleMimeType= */ MimeTypes.TEXT_VTT,
|
||||
/* codecs= */ null,
|
||||
/* bitrate= */ Format.NO_VALUE,
|
||||
/* selectionFlags= */ 0,
|
||||
language);
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user