From 9779f2c358913a947e02bd19aa7c423d677e5c28 Mon Sep 17 00:00:00 2001 From: tonihei Date: Tue, 29 Jan 2019 12:57:37 +0000 Subject: [PATCH] Add DashMediaPeriod getStreamKeys implementation and test. PiperOrigin-RevId: 231385518 --- .../source/dash/DashMediaPeriod.java | 48 +++- .../source/dash/DashMediaPeriodTest.java | 247 ++++++++++++++++++ .../smoothstreaming/SsMediaPeriodTest.java | 2 +- .../testutil/MediaPeriodAsserts.java | 47 +++- 4 files changed, 333 insertions(+), 11 deletions(-) create mode 100644 library/dash/src/test/java/com/google/android/exoplayer2/source/dash/DashMediaPeriodTest.java diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaPeriod.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaPeriod.java index cba7a9941b..fd0453e79e 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaPeriod.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaPeriod.java @@ -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 getStreamKeys(List trackSelections) { + List manifestAdaptationSets = manifest.getPeriod(periodIndex).adaptationSets; + List 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, diff --git a/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/DashMediaPeriodTest.java b/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/DashMediaPeriodTest.java new file mode 100644 index 0000000000..0d9fee282c --- /dev/null +++ b/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/DashMediaPeriodTest.java @@ -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 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"); + } +} diff --git a/library/smoothstreaming/src/test/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaPeriodTest.java b/library/smoothstreaming/src/test/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaPeriodTest.java index 54de4badbd..bceaf8cdf2 100644 --- a/library/smoothstreaming/src/test/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaPeriodTest.java +++ b/library/smoothstreaming/src/test/java/com/google/android/exoplayer2/source/smoothstreaming/SsMediaPeriodTest.java @@ -77,7 +77,7 @@ public class SsMediaPeriodTest { mock(Allocator.class)); MediaPeriodAsserts.assertGetStreamKeysAndManifestFilterIntegration( - mediaPeriodFactory, testManifest, /* periodIndex= */ 0); + mediaPeriodFactory, testManifest); } private static Format createVideoFormat(int bitrate) { diff --git a/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/MediaPeriodAsserts.java b/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/MediaPeriodAsserts.java index 5235163684..b4137a41de 100644 --- a/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/MediaPeriodAsserts.java +++ b/testutils_robolectric/src/main/java/com/google/android/exoplayer2/testutil/MediaPeriodAsserts.java @@ -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 > void assertGetStreamKeysAndManifestFilterIntegration( - FilterableManifestMediaPeriodFactory mediaPeriodFactory, T manifest, int periodIndex) { + FilterableManifestMediaPeriodFactory 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 > + void assertGetStreamKeysAndManifestFilterIntegration( + FilterableManifestMediaPeriodFactory 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 selectionsFromAllGroups = new ArrayList<>(); @@ -113,12 +136,20 @@ public final class MediaPeriodAsserts { // contain at least all requested formats. for (List testSelection : testSelections) { List 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);