Add HlsMediaPeriod getStreamKeys implementation and tests.

PiperOrigin-RevId: 231385563
This commit is contained in:
tonihei 2019-01-29 12:58:10 +00:00 committed by Oliver Woodman
parent 6a52cd7445
commit 32b40502fc
3 changed files with 305 additions and 23 deletions

View File

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

View File

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

View File

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