Add DashMediaPeriod getStreamKeys implementation and test.

PiperOrigin-RevId: 231385518
This commit is contained in:
tonihei 2019-01-29 12:57:37 +00:00 committed by Oliver Woodman
parent 92bf8e918c
commit 9779f2c358
4 changed files with 333 additions and 11 deletions

View file

@ -22,6 +22,7 @@ import android.util.SparseIntArray;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.SeekParameters;
import com.google.android.exoplayer2.offline.StreamKey;
import com.google.android.exoplayer2.source.CompositeSequenceableLoaderFactory;
import com.google.android.exoplayer2.source.EmptySampleStream;
import com.google.android.exoplayer2.source.MediaPeriod;
@ -192,6 +193,49 @@ import java.util.List;
return trackGroups;
}
@Override
public List<StreamKey> getStreamKeys(List<TrackSelection> trackSelections) {
List<AdaptationSet> manifestAdaptationSets = manifest.getPeriod(periodIndex).adaptationSets;
List<StreamKey> streamKeys = new ArrayList<>();
for (TrackSelection trackSelection : trackSelections) {
int trackGroupIndex = trackGroups.indexOf(trackSelection.getTrackGroup());
TrackGroupInfo trackGroupInfo = trackGroupInfos[trackGroupIndex];
if (trackGroupInfo.trackGroupCategory != TrackGroupInfo.CATEGORY_PRIMARY) {
// Ignore non-primary tracks.
continue;
}
int[] adaptationSetIndices = trackGroupInfo.adaptationSetIndices;
int[] trackIndices = new int[trackSelection.length()];
for (int i = 0; i < trackSelection.length(); i++) {
trackIndices[i] = trackSelection.getIndexInTrackGroup(i);
}
Arrays.sort(trackIndices);
int currentAdaptationSetIndex = 0;
int totalTracksInPreviousAdaptationSets = 0;
int tracksInCurrentAdaptationSet =
manifestAdaptationSets.get(adaptationSetIndices[0]).representations.size();
for (int i = 0; i < trackIndices.length; i++) {
while (trackIndices[i]
>= totalTracksInPreviousAdaptationSets + tracksInCurrentAdaptationSet) {
currentAdaptationSetIndex++;
totalTracksInPreviousAdaptationSets += tracksInCurrentAdaptationSet;
tracksInCurrentAdaptationSet =
manifestAdaptationSets
.get(adaptationSetIndices[currentAdaptationSetIndex])
.representations
.size();
}
streamKeys.add(
new StreamKey(
periodIndex,
adaptationSetIndices[currentAdaptationSetIndex],
trackIndices[i] - totalTracksInPreviousAdaptationSets));
}
}
return streamKeys;
}
@Override
public long selectTracks(TrackSelection[] selections, boolean[] mayRetainStreamFlags,
SampleStream[] streams, boolean[] streamResetFlags, long positionUs) {
@ -697,7 +741,7 @@ import java.util.List;
public final int[] adaptationSetIndices;
public final int trackType;
public @TrackGroupCategory final int trackGroupCategory;
@TrackGroupCategory public final int trackGroupCategory;
public final int eventStreamGroupIndex;
public final int primaryTrackGroupIndex;
@ -748,7 +792,7 @@ import java.util.List;
return new TrackGroupInfo(
C.TRACK_TYPE_METADATA,
CATEGORY_MANIFEST_EVENTS,
null,
new int[0],
-1,
C.INDEX_UNSET,
C.INDEX_UNSET,

View file

@ -0,0 +1,247 @@
/*
* 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.dash;
import static org.mockito.Mockito.mock;
import android.net.Uri;
import android.support.annotation.Nullable;
import com.google.android.exoplayer2.C;
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.dash.PlayerEmsgHandler.PlayerEmsgCallback;
import com.google.android.exoplayer2.source.dash.manifest.AdaptationSet;
import com.google.android.exoplayer2.source.dash.manifest.DashManifest;
import com.google.android.exoplayer2.source.dash.manifest.Descriptor;
import com.google.android.exoplayer2.source.dash.manifest.Period;
import com.google.android.exoplayer2.source.dash.manifest.Representation;
import com.google.android.exoplayer2.source.dash.manifest.SegmentBase.SingleSegmentBase;
import com.google.android.exoplayer2.source.dash.manifest.UtcTimingElement;
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.LoadErrorHandlingPolicy;
import com.google.android.exoplayer2.upstream.LoaderErrorThrower;
import com.google.android.exoplayer2.upstream.TransferListener;
import com.google.android.exoplayer2.util.MimeTypes;
import java.util.Arrays;
import java.util.Collections;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.Config;
/** Unit tests for {@link DashMediaPeriod}. */
@RunWith(RobolectricTestRunner.class)
@Config(shadows = {RobolectricUtil.CustomLooper.class, RobolectricUtil.CustomMessageQueue.class})
public final class DashMediaPeriodTest {
@Test
public void getSteamKeys_isCompatibleWithDashManifestFilter() {
// Test manifest which covers various edge cases:
// - Multiple periods.
// - Single and multiple representations per adaptation set.
// - Switch descriptors combining multiple adaptations sets.
// - Embedded track groups.
// All cases are deliberately combined in one test to catch potential indexing problems which
// only occur in combination.
DashManifest testManifest =
createDashManifest(
createPeriod(
createAdaptationSet(
/* id= */ 0,
/* trackType= */ C.TRACK_TYPE_VIDEO,
/* descriptor= */ null,
createVideoRepresentation(/* bitrate= */ 1000000))),
createPeriod(
createAdaptationSet(
/* id= */ 100,
/* trackType= */ C.TRACK_TYPE_VIDEO,
/* descriptor= */ createSwitchDescriptor(/* ids= */ 103, 104),
createVideoRepresentationWithInbandEventStream(/* bitrate= */ 200000),
createVideoRepresentationWithInbandEventStream(/* bitrate= */ 400000),
createVideoRepresentationWithInbandEventStream(/* bitrate= */ 600000)),
createAdaptationSet(
/* id= */ 101,
/* trackType= */ C.TRACK_TYPE_AUDIO,
/* descriptor= */ createSwitchDescriptor(/* ids= */ 102),
createAudioRepresentation(/* bitrate= */ 48000),
createAudioRepresentation(/* bitrate= */ 96000)),
createAdaptationSet(
/* id= */ 102,
/* trackType= */ C.TRACK_TYPE_AUDIO,
/* descriptor= */ createSwitchDescriptor(/* ids= */ 101),
createAudioRepresentation(/* bitrate= */ 256000)),
createAdaptationSet(
/* id= */ 103,
/* trackType= */ C.TRACK_TYPE_VIDEO,
/* descriptor= */ createSwitchDescriptor(/* ids= */ 100, 104),
createVideoRepresentationWithInbandEventStream(/* bitrate= */ 800000),
createVideoRepresentationWithInbandEventStream(/* bitrate= */ 1000000)),
createAdaptationSet(
/* id= */ 104,
/* trackType= */ C.TRACK_TYPE_VIDEO,
/* descriptor= */ createSwitchDescriptor(/* ids= */ 100, 103),
createVideoRepresentationWithInbandEventStream(/* bitrate= */ 2000000)),
createAdaptationSet(
/* id= */ 105,
/* trackType= */ C.TRACK_TYPE_TEXT,
/* descriptor= */ null,
createTextRepresentation(/* language= */ "eng")),
createAdaptationSet(
/* id= */ 105,
/* trackType= */ C.TRACK_TYPE_TEXT,
/* descriptor= */ null,
createTextRepresentation(/* language= */ "ger"))));
FilterableManifestMediaPeriodFactory<DashManifest> mediaPeriodFactory =
(manifest, periodIndex) ->
new DashMediaPeriod(
/* id= */ periodIndex,
manifest,
periodIndex,
mock(DashChunkSource.Factory.class),
mock(TransferListener.class),
mock(LoadErrorHandlingPolicy.class),
new EventDispatcher()
.withParameters(
/* windowIndex= */ 0,
/* mediaPeriodId= */ new MediaPeriodId(/* periodUid= */ new Object()),
/* mediaTimeOffsetMs= */ 0),
/* elapsedRealtimeOffsetMs= */ 0,
mock(LoaderErrorThrower.class),
mock(Allocator.class),
mock(CompositeSequenceableLoaderFactory.class),
mock(PlayerEmsgCallback.class));
// Ignore embedded metadata as we don't want to select primary group just to get embedded track.
MediaPeriodAsserts.assertGetStreamKeysAndManifestFilterIntegration(
mediaPeriodFactory,
testManifest,
/* periodIndex= */ 1,
/* ignoredMimeType= */ "application/x-emsg");
}
private static DashManifest createDashManifest(Period... periods) {
return new DashManifest(
/* availabilityStartTimeMs= */ 0,
/* durationMs= */ 5000,
/* minBufferTimeMs= */ 1,
/* dynamic= */ false,
/* minUpdatePeriodMs= */ 2,
/* timeShiftBufferDepthMs= */ 3,
/* suggestedPresentationDelayMs= */ 4,
/* publishTimeMs= */ 12345,
/* programInformation= */ null,
new UtcTimingElement("", ""),
Uri.EMPTY,
Arrays.asList(periods));
}
private static Period createPeriod(AdaptationSet... adaptationSets) {
return new Period(/* id= */ null, /* startMs= */ 0, Arrays.asList(adaptationSets));
}
private static AdaptationSet createAdaptationSet(
int id, int trackType, @Nullable Descriptor descriptor, Representation... representations) {
return new AdaptationSet(
id,
trackType,
Arrays.asList(representations),
/* accessibilityDescriptors= */ Collections.emptyList(),
descriptor == null ? Collections.emptyList() : Collections.singletonList(descriptor));
}
private static Representation createVideoRepresentation(int bitrate) {
return Representation.newInstance(
/* revisionId= */ 0,
createVideoFormat(bitrate),
/* baseUrl= */ "",
new SingleSegmentBase());
}
private static Representation createVideoRepresentationWithInbandEventStream(int bitrate) {
return Representation.newInstance(
/* revisionId= */ 0,
createVideoFormat(bitrate),
/* baseUrl= */ "",
new SingleSegmentBase(),
Collections.singletonList(getInbandEventDescriptor()));
}
private static Format createVideoFormat(int bitrate) {
return Format.createContainerFormat(
/* id= */ null,
/* label= */ null,
MimeTypes.VIDEO_MP4,
MimeTypes.VIDEO_H264,
/* codecs= */ null,
bitrate,
/* selectionFlags= */ 0,
/* language= */ null);
}
private static Representation createAudioRepresentation(int bitrate) {
return Representation.newInstance(
/* revisionId= */ 0,
Format.createContainerFormat(
/* id= */ null,
/* label= */ null,
MimeTypes.AUDIO_MP4,
MimeTypes.AUDIO_AAC,
/* codecs= */ null,
bitrate,
/* selectionFlags= */ 0,
/* language= */ null),
/* baseUrl= */ "",
new SingleSegmentBase());
}
private static Representation createTextRepresentation(String language) {
return Representation.newInstance(
/* revisionId= */ 0,
Format.createContainerFormat(
/* id= */ null,
/* label= */ null,
MimeTypes.APPLICATION_MP4,
MimeTypes.TEXT_VTT,
/* codecs= */ null,
/* bitrate= */ Format.NO_VALUE,
/* selectionFlags= */ 0,
language),
/* baseUrl= */ "",
new SingleSegmentBase());
}
private static Descriptor createSwitchDescriptor(int... ids) {
StringBuilder idString = new StringBuilder();
idString.append(ids[0]);
for (int i = 1; i < ids.length; i++) {
idString.append(",").append(ids[i]);
}
return new Descriptor(
/* schemeIdUri= */ "urn:mpeg:dash:adaptation-set-switching:2016",
/* value= */ idString.toString(),
/* id= */ null);
}
private static Descriptor getInbandEventDescriptor() {
return new Descriptor(
/* schemeIdUri= */ "inBandSchemeIdUri", /* value= */ "inBandValue", /* id= */ "inBandId");
}
}

View file

@ -77,7 +77,7 @@ public class SsMediaPeriodTest {
mock(Allocator.class));
MediaPeriodAsserts.assertGetStreamKeysAndManifestFilterIntegration(
mediaPeriodFactory, testManifest, /* periodIndex= */ 0);
mediaPeriodFactory, testManifest);
}
private static Format createVideoFormat(int bitrate) {

View file

@ -58,11 +58,30 @@ public final class MediaPeriodAsserts {
*
* @param mediaPeriodFactory A factory to create a {@link MediaPeriod} based on a manifest.
* @param manifest The manifest which is to be tested.
* @param periodIndex The index of period in the manifest.
*/
public static <T extends FilterableManifest<T>>
void assertGetStreamKeysAndManifestFilterIntegration(
FilterableManifestMediaPeriodFactory<T> mediaPeriodFactory, T manifest, int periodIndex) {
FilterableManifestMediaPeriodFactory<T> mediaPeriodFactory, T manifest) {
assertGetStreamKeysAndManifestFilterIntegration(
mediaPeriodFactory, manifest, /* periodIndex= */ 0, /* ignoredMimeType= */ null);
}
/**
* Asserts that the values returns by {@link MediaPeriod#getStreamKeys(List)} are compatible with
* a {@link FilterableManifest} using these stream keys.
*
* @param mediaPeriodFactory A factory to create a {@link MediaPeriod} based on a manifest.
* @param manifest The manifest which is to be tested.
* @param periodIndex The index of period in the manifest.
* @param ignoredMimeType Optional mime type whose existence in the filtered track groups is not
* asserted.
*/
public static <T extends FilterableManifest<T>>
void assertGetStreamKeysAndManifestFilterIntegration(
FilterableManifestMediaPeriodFactory<T> mediaPeriodFactory,
T manifest,
int periodIndex,
@Nullable String ignoredMimeType) {
MediaPeriod mediaPeriod = mediaPeriodFactory.createMediaPeriod(manifest, periodIndex);
TrackGroupArray trackGroupArray = getTrackGroups(mediaPeriod);
@ -94,12 +113,16 @@ public final class MediaPeriodAsserts {
}
}
if (trackGroupArray.length > 1) {
testSelections.add(
Arrays.asList(
new TrackSelection[] {
new TestTrackSelection(trackGroupArray.get(0), 0),
new TestTrackSelection(trackGroupArray.get(1), 0)
}));
for (int i = 0; i < trackGroupArray.length - 1; i++) {
for (int j = i + 1; j < trackGroupArray.length; j++) {
testSelections.add(
Arrays.asList(
new TrackSelection[] {
new TestTrackSelection(trackGroupArray.get(i), 0),
new TestTrackSelection(trackGroupArray.get(j), 0)
}));
}
}
}
if (trackGroupArray.length > 2) {
List<TrackSelection> selectionsFromAllGroups = new ArrayList<>();
@ -113,12 +136,20 @@ public final class MediaPeriodAsserts {
// contain at least all requested formats.
for (List<TrackSelection> testSelection : testSelections) {
List<StreamKey> streamKeys = mediaPeriod.getStreamKeys(testSelection);
if (streamKeys.isEmpty()) {
// Manifests won't be filtered if stream key is empty.
continue;
}
T filteredManifest = manifest.copy(streamKeys);
// The filtered manifest should only have one period left.
MediaPeriod filteredMediaPeriod =
mediaPeriodFactory.createMediaPeriod(filteredManifest, /* periodIndex= */ 0);
TrackGroupArray filteredTrackGroupArray = getTrackGroups(filteredMediaPeriod);
for (TrackSelection trackSelection : testSelection) {
if (ignoredMimeType != null
&& ignoredMimeType.equals(trackSelection.getFormat(0).sampleMimeType)) {
continue;
}
Format[] expectedFormats = new Format[trackSelection.length()];
for (int k = 0; k < trackSelection.length(); k++) {
expectedFormats[k] = trackSelection.getFormat(k);