mirror of
https://github.com/samsonjs/media.git
synced 2026-04-14 12:45:47 +00:00
Add DashMediaPeriod getStreamKeys implementation and test.
PiperOrigin-RevId: 231385518
This commit is contained in:
parent
92bf8e918c
commit
9779f2c358
4 changed files with 333 additions and 11 deletions
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
|
@ -77,7 +77,7 @@ public class SsMediaPeriodTest {
|
|||
mock(Allocator.class));
|
||||
|
||||
MediaPeriodAsserts.assertGetStreamKeysAndManifestFilterIntegration(
|
||||
mediaPeriodFactory, testManifest, /* periodIndex= */ 0);
|
||||
mediaPeriodFactory, testManifest);
|
||||
}
|
||||
|
||||
private static Format createVideoFormat(int bitrate) {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
Loading…
Reference in a new issue