Add BaseUrlExlusionList and use it to select base URLs

Issues: #771 and #7654
PiperOrigin-RevId: 386850707
This commit is contained in:
bachinger 2021-07-26 13:31:17 +01:00
parent c6e860b8bb
commit 3f9093cc02
10 changed files with 678 additions and 23 deletions

View File

@ -47,6 +47,9 @@
`TrackSelection#getFormat()`. `TrackSelection#getFormat()`.
* Deprecate `ControlDispatcher` and `DefaultControlDispatcher`. Use a * Deprecate `ControlDispatcher` and `DefaultControlDispatcher`. Use a
`ForwardingPlayer` or configure the player to customize operations. `ForwardingPlayer` or configure the player to customize operations.
* Change interface of `LoadErrorHandlingPolicy` to support configuring the
behavior of track and location fallback. Location fallback is currently
only supported for DASH manifests with multiple base URLs.
* Remove deprecated symbols: * Remove deprecated symbols:
* Remove `Player.getPlaybackError`. Use `Player.getPlayerError` instead. * Remove `Player.getPlaybackError`. Use `Player.getPlayerError` instead.
* Remove `Player.getCurrentTag`. Use `Player.getCurrentMediaItem` and * Remove `Player.getCurrentTag`. Use `Player.getCurrentMediaItem` and
@ -160,6 +163,12 @@
([#9158](https://github.com/google/ExoPlayer/issues/9158)). ([#9158](https://github.com/google/ExoPlayer/issues/9158)).
* Fix issue around TS synchronization when reading a file's duration * Fix issue around TS synchronization when reading a file's duration
([#9100](https://github.com/google/ExoPlayer/pull/9100)). ([#9100](https://github.com/google/ExoPlayer/pull/9100)).
* DASH:
* Add support for multiple base URLs and DVB attributes in the manifest.
Apps that are using `DefaultLoadErrorHandlingPolicy` with such manifests
have base URL fallback automatically enabled
([#771](https://github.com/google/ExoPlayer/issues/771) and
[#7654](https://github.com/google/ExoPlayer/issues/7654)).
* HLS: * HLS:
* Fix issue where playback of a live event could become stuck rather than * Fix issue where playback of a live event could become stuck rather than
transitioning to `STATE_ENDED` when the event ends transitioning to `STATE_ENDED` when the event ends

View File

@ -0,0 +1,210 @@
/*
* Copyright (C) 2021 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 com.google.android.exoplayer2.util.Util.castNonNull;
import static java.lang.Math.max;
import android.os.SystemClock;
import android.util.Pair;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import com.google.android.exoplayer2.source.dash.manifest.BaseUrl;
import com.google.common.collect.Iterables;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.Set;
/**
* Holds the state of {@link #exclude(BaseUrl, long) excluded} base URLs to be used {@link
* #selectBaseUrl(List) to select} a base URL based on these exclusions.
*/
public final class BaseUrlExclusionList {
private final Map<String, Long> excludedServiceLocations;
private final Map<Integer, Long> excludedPriorities;
private final Map<List<Pair<String, Integer>>, BaseUrl> selectionsTaken = new HashMap<>();
private final Random random;
/** Creates an instance. */
public BaseUrlExclusionList() {
this(new Random());
}
/** Creates an instance with the given {@link Random}. */
@VisibleForTesting
/* package */ BaseUrlExclusionList(Random random) {
this.random = random;
excludedServiceLocations = new HashMap<>();
excludedPriorities = new HashMap<>();
}
/**
* Excludes the given base URL.
*
* @param baseUrlToExclude The base URL to exclude.
* @param exclusionDurationMs The duration of exclusion, in milliseconds.
*/
public void exclude(BaseUrl baseUrlToExclude, long exclusionDurationMs) {
long excludeUntilMs = SystemClock.elapsedRealtime() + exclusionDurationMs;
addExclusion(baseUrlToExclude.serviceLocation, excludeUntilMs, excludedServiceLocations);
addExclusion(baseUrlToExclude.priority, excludeUntilMs, excludedPriorities);
}
/**
* Selects the base URL to use from the given list.
*
* <p>The list is reduced by service location and priority of base URLs that have been passed to
* {@link #exclude(BaseUrl, long)}. The base URL to use is then selected from the remaining base
* URLs by priority and weight.
*
* @param baseUrls The list of {@link BaseUrl base URLs} to select from.
* @return The selected base URL after exclusion or null if all elements have been excluded.
*/
@Nullable
public BaseUrl selectBaseUrl(List<BaseUrl> baseUrls) {
List<BaseUrl> includedBaseUrls = applyExclusions(baseUrls);
if (includedBaseUrls.size() < 2) {
return Iterables.getFirst(includedBaseUrls, /* defaultValue= */ null);
}
// Sort by priority and service location to make the sort order of the candidates deterministic.
Collections.sort(includedBaseUrls, BaseUrlExclusionList::compareBaseUrl);
// Get candidates of the lowest priority from the head of the sorted list.
List<Pair<String, Integer>> candidateKeys = new ArrayList<>();
int lowestPriority = includedBaseUrls.get(0).priority;
for (int i = 0; i < includedBaseUrls.size(); i++) {
BaseUrl baseUrl = includedBaseUrls.get(i);
if (lowestPriority != baseUrl.priority) {
if (candidateKeys.size() == 1) {
// Only a single candidate of lowest priority; no choice.
return includedBaseUrls.get(0);
}
break;
}
candidateKeys.add(new Pair<>(baseUrl.serviceLocation, baseUrl.weight));
}
// Check whether selection has already been taken.
@Nullable BaseUrl baseUrl = selectionsTaken.get(candidateKeys);
if (baseUrl == null) {
// Weighted random selection from multiple candidates of the same priority.
baseUrl = selectWeighted(includedBaseUrls.subList(0, candidateKeys.size()));
// Remember the selection taken for later.
selectionsTaken.put(candidateKeys, baseUrl);
}
return baseUrl;
}
/**
* Returns the number of priority levels for the given list of base URLs after exclusion.
*
* @param baseUrls The list of base URLs.
* @return The number of priority levels after exclusion.
*/
public int getPriorityCountAfterExclusion(List<BaseUrl> baseUrls) {
Set<Integer> priorities = new HashSet<>();
List<BaseUrl> includedBaseUrls = applyExclusions(baseUrls);
for (int i = 0; i < includedBaseUrls.size(); i++) {
priorities.add(includedBaseUrls.get(i).priority);
}
return priorities.size();
}
/**
* Returns the number of priority levels of the given list of base URLs.
*
* @param baseUrls The list of base URLs.
* @return The number of priority levels before exclusion.
*/
public static int getPriorityCount(List<BaseUrl> baseUrls) {
Set<Integer> priorities = new HashSet<>();
for (int i = 0; i < baseUrls.size(); i++) {
priorities.add(baseUrls.get(i).priority);
}
return priorities.size();
}
/** Resets the state. */
public void reset() {
excludedServiceLocations.clear();
excludedPriorities.clear();
selectionsTaken.clear();
}
// Internal methods.
private List<BaseUrl> applyExclusions(List<BaseUrl> baseUrls) {
long nowMs = SystemClock.elapsedRealtime();
removeExpiredExclusions(nowMs, excludedServiceLocations);
removeExpiredExclusions(nowMs, excludedPriorities);
List<BaseUrl> includedBaseUrls = new ArrayList<>();
for (int i = 0; i < baseUrls.size(); i++) {
BaseUrl baseUrl = baseUrls.get(i);
if (!excludedServiceLocations.containsKey(baseUrl.serviceLocation)
&& !excludedPriorities.containsKey(baseUrl.priority)) {
includedBaseUrls.add(baseUrl);
}
}
return includedBaseUrls;
}
private BaseUrl selectWeighted(List<BaseUrl> candidates) {
int totalWeight = 0;
for (int i = 0; i < candidates.size(); i++) {
totalWeight += candidates.get(i).weight;
}
int randomChoice = random.nextInt(/* bound= */ totalWeight);
totalWeight = 0;
for (int i = 0; i < candidates.size(); i++) {
BaseUrl baseUrl = candidates.get(i);
totalWeight += baseUrl.weight;
if (randomChoice < totalWeight) {
return baseUrl;
}
}
return Iterables.getLast(candidates);
}
private static <T> void addExclusion(
T toExclude, long excludeUntilMs, Map<T, Long> currentExclusions) {
if (currentExclusions.containsKey(toExclude)) {
excludeUntilMs = max(excludeUntilMs, castNonNull(currentExclusions.get(toExclude)));
}
currentExclusions.put(toExclude, excludeUntilMs);
}
private static <T> void removeExpiredExclusions(long nowMs, Map<T, Long> exclusions) {
List<T> expiredExclusions = new ArrayList<>();
for (Map.Entry<T, Long> entries : exclusions.entrySet()) {
if (entries.getValue() <= nowMs) {
expiredExclusions.add(entries.getKey());
}
}
for (int i = 0; i < expiredExclusions.size(); i++) {
exclusions.remove(expiredExclusions.get(i));
}
}
/** Compare by priority and service location. */
private static int compareBaseUrl(BaseUrl a, BaseUrl b) {
int compare = Integer.compare(a.priority, b.priority);
return compare != 0 ? compare : a.serviceLocation.compareTo(b.serviceLocation);
}
}

View File

@ -35,6 +35,7 @@ public interface DashChunkSource extends ChunkSource {
/** /**
* @param manifestLoaderErrorThrower Throws errors affecting loading of manifests. * @param manifestLoaderErrorThrower Throws errors affecting loading of manifests.
* @param manifest The initial manifest. * @param manifest The initial manifest.
* @param baseUrlExclusionList The base URL exclusion list.
* @param periodIndex The index of the corresponding period in the manifest. * @param periodIndex The index of the corresponding period in the manifest.
* @param adaptationSetIndices The indices of the corresponding adaptation sets in the period. * @param adaptationSetIndices The indices of the corresponding adaptation sets in the period.
* @param trackSelection The track selection. * @param trackSelection The track selection.
@ -51,6 +52,7 @@ public interface DashChunkSource extends ChunkSource {
DashChunkSource createDashChunkSource( DashChunkSource createDashChunkSource(
LoaderErrorThrower manifestLoaderErrorThrower, LoaderErrorThrower manifestLoaderErrorThrower,
DashManifest manifest, DashManifest manifest,
BaseUrlExclusionList baseUrlExclusionList,
int periodIndex, int periodIndex,
int[] adaptationSetIndices, int[] adaptationSetIndices,
ExoTrackSelection trackSelection, ExoTrackSelection trackSelection,

View File

@ -84,6 +84,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
@Nullable private final TransferListener transferListener; @Nullable private final TransferListener transferListener;
private final DrmSessionManager drmSessionManager; private final DrmSessionManager drmSessionManager;
private final LoadErrorHandlingPolicy loadErrorHandlingPolicy; private final LoadErrorHandlingPolicy loadErrorHandlingPolicy;
private final BaseUrlExclusionList baseUrlExclusionList;
private final long elapsedRealtimeOffsetMs; private final long elapsedRealtimeOffsetMs;
private final LoaderErrorThrower manifestLoaderErrorThrower; private final LoaderErrorThrower manifestLoaderErrorThrower;
private final Allocator allocator; private final Allocator allocator;
@ -107,6 +108,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
public DashMediaPeriod( public DashMediaPeriod(
int id, int id,
DashManifest manifest, DashManifest manifest,
BaseUrlExclusionList baseUrlExclusionList,
int periodIndex, int periodIndex,
DashChunkSource.Factory chunkSourceFactory, DashChunkSource.Factory chunkSourceFactory,
@Nullable TransferListener transferListener, @Nullable TransferListener transferListener,
@ -121,6 +123,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
PlayerEmsgCallback playerEmsgCallback) { PlayerEmsgCallback playerEmsgCallback) {
this.id = id; this.id = id;
this.manifest = manifest; this.manifest = manifest;
this.baseUrlExclusionList = baseUrlExclusionList;
this.periodIndex = periodIndex; this.periodIndex = periodIndex;
this.chunkSourceFactory = chunkSourceFactory; this.chunkSourceFactory = chunkSourceFactory;
this.transferListener = transferListener; this.transferListener = transferListener;
@ -766,6 +769,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
chunkSourceFactory.createDashChunkSource( chunkSourceFactory.createDashChunkSource(
manifestLoaderErrorThrower, manifestLoaderErrorThrower,
manifest, manifest,
baseUrlExclusionList,
periodIndex, periodIndex,
trackGroupInfo.adaptationSetIndices, trackGroupInfo.adaptationSetIndices,
selection, selection,

View File

@ -448,6 +448,7 @@ public final class DashMediaSource extends BaseMediaSource {
private final CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory; private final CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory;
private final DrmSessionManager drmSessionManager; private final DrmSessionManager drmSessionManager;
private final LoadErrorHandlingPolicy loadErrorHandlingPolicy; private final LoadErrorHandlingPolicy loadErrorHandlingPolicy;
private final BaseUrlExclusionList baseUrlExclusionList;
private final long fallbackTargetLiveOffsetMs; private final long fallbackTargetLiveOffsetMs;
private final EventDispatcher manifestEventDispatcher; private final EventDispatcher manifestEventDispatcher;
private final ParsingLoadable.Parser<? extends DashManifest> manifestParser; private final ParsingLoadable.Parser<? extends DashManifest> manifestParser;
@ -502,6 +503,7 @@ public final class DashMediaSource extends BaseMediaSource {
this.loadErrorHandlingPolicy = loadErrorHandlingPolicy; this.loadErrorHandlingPolicy = loadErrorHandlingPolicy;
this.fallbackTargetLiveOffsetMs = fallbackTargetLiveOffsetMs; this.fallbackTargetLiveOffsetMs = fallbackTargetLiveOffsetMs;
this.compositeSequenceableLoaderFactory = compositeSequenceableLoaderFactory; this.compositeSequenceableLoaderFactory = compositeSequenceableLoaderFactory;
baseUrlExclusionList = new BaseUrlExclusionList();
sideloadedManifest = manifest != null; sideloadedManifest = manifest != null;
manifestEventDispatcher = createEventDispatcher(/* mediaPeriodId= */ null); manifestEventDispatcher = createEventDispatcher(/* mediaPeriodId= */ null);
manifestUriLock = new Object(); manifestUriLock = new Object();
@ -572,6 +574,7 @@ public final class DashMediaSource extends BaseMediaSource {
new DashMediaPeriod( new DashMediaPeriod(
firstPeriodId + periodIndex, firstPeriodId + periodIndex,
manifest, manifest,
baseUrlExclusionList,
periodIndex, periodIndex,
chunkSourceFactory, chunkSourceFactory,
mediaTransferListener, mediaTransferListener,
@ -617,6 +620,7 @@ public final class DashMediaSource extends BaseMediaSource {
expiredManifestPublishTimeUs = C.TIME_UNSET; expiredManifestPublishTimeUs = C.TIME_UNSET;
firstPeriodId = 0; firstPeriodId = 0;
periodsById.clear(); periodsById.clear();
baseUrlExclusionList.reset();
drmSessionManager.release(); drmSessionManager.release();
} }

View File

@ -15,8 +15,6 @@
*/ */
package com.google.android.exoplayer2.source.dash; package com.google.android.exoplayer2.source.dash;
import static com.google.android.exoplayer2.trackselection.TrackSelectionUtil.createFallbackOptions;
import static com.google.android.exoplayer2.upstream.LoadErrorHandlingPolicy.FALLBACK_TYPE_TRACK;
import static java.lang.Math.max; import static java.lang.Math.max;
import static java.lang.Math.min; import static java.lang.Math.min;
@ -103,6 +101,7 @@ public class DefaultDashChunkSource implements DashChunkSource {
public DashChunkSource createDashChunkSource( public DashChunkSource createDashChunkSource(
LoaderErrorThrower manifestLoaderErrorThrower, LoaderErrorThrower manifestLoaderErrorThrower,
DashManifest manifest, DashManifest manifest,
BaseUrlExclusionList baseUrlExclusionList,
int periodIndex, int periodIndex,
int[] adaptationSetIndices, int[] adaptationSetIndices,
ExoTrackSelection trackSelection, ExoTrackSelection trackSelection,
@ -120,6 +119,7 @@ public class DefaultDashChunkSource implements DashChunkSource {
chunkExtractorFactory, chunkExtractorFactory,
manifestLoaderErrorThrower, manifestLoaderErrorThrower,
manifest, manifest,
baseUrlExclusionList,
periodIndex, periodIndex,
adaptationSetIndices, adaptationSetIndices,
trackSelection, trackSelection,
@ -134,6 +134,7 @@ public class DefaultDashChunkSource implements DashChunkSource {
} }
private final LoaderErrorThrower manifestLoaderErrorThrower; private final LoaderErrorThrower manifestLoaderErrorThrower;
private final BaseUrlExclusionList baseUrlExclusionList;
private final int[] adaptationSetIndices; private final int[] adaptationSetIndices;
private final int trackType; private final int trackType;
private final DataSource dataSource; private final DataSource dataSource;
@ -154,6 +155,7 @@ public class DefaultDashChunkSource implements DashChunkSource {
* chunks. * chunks.
* @param manifestLoaderErrorThrower Throws errors affecting loading of manifests. * @param manifestLoaderErrorThrower Throws errors affecting loading of manifests.
* @param manifest The initial manifest. * @param manifest The initial manifest.
* @param baseUrlExclusionList The base URL exclusion list.
* @param periodIndex The index of the period in the manifest. * @param periodIndex The index of the period in the manifest.
* @param adaptationSetIndices The indices of the adaptation sets in the period. * @param adaptationSetIndices The indices of the adaptation sets in the period.
* @param trackSelection The track selection. * @param trackSelection The track selection.
@ -174,6 +176,7 @@ public class DefaultDashChunkSource implements DashChunkSource {
ChunkExtractor.Factory chunkExtractorFactory, ChunkExtractor.Factory chunkExtractorFactory,
LoaderErrorThrower manifestLoaderErrorThrower, LoaderErrorThrower manifestLoaderErrorThrower,
DashManifest manifest, DashManifest manifest,
BaseUrlExclusionList baseUrlExclusionList,
int periodIndex, int periodIndex,
int[] adaptationSetIndices, int[] adaptationSetIndices,
ExoTrackSelection trackSelection, ExoTrackSelection trackSelection,
@ -186,6 +189,7 @@ public class DefaultDashChunkSource implements DashChunkSource {
@Nullable PlayerTrackEmsgHandler playerTrackEmsgHandler) { @Nullable PlayerTrackEmsgHandler playerTrackEmsgHandler) {
this.manifestLoaderErrorThrower = manifestLoaderErrorThrower; this.manifestLoaderErrorThrower = manifestLoaderErrorThrower;
this.manifest = manifest; this.manifest = manifest;
this.baseUrlExclusionList = baseUrlExclusionList;
this.adaptationSetIndices = adaptationSetIndices; this.adaptationSetIndices = adaptationSetIndices;
this.trackSelection = trackSelection; this.trackSelection = trackSelection;
this.trackType = trackType; this.trackType = trackType;
@ -201,11 +205,13 @@ public class DefaultDashChunkSource implements DashChunkSource {
representationHolders = new RepresentationHolder[trackSelection.length()]; representationHolders = new RepresentationHolder[trackSelection.length()];
for (int i = 0; i < representationHolders.length; i++) { for (int i = 0; i < representationHolders.length; i++) {
Representation representation = representations.get(trackSelection.getIndexInTrackGroup(i)); Representation representation = representations.get(trackSelection.getIndexInTrackGroup(i));
@Nullable
BaseUrl selectedBaseUrl = baseUrlExclusionList.selectBaseUrl(representation.baseUrls);
representationHolders[i] = representationHolders[i] =
new RepresentationHolder( new RepresentationHolder(
periodDurationUs, periodDurationUs,
representation, representation,
representation.baseUrls.get(0), selectedBaseUrl != null ? selectedBaseUrl : representation.baseUrls.get(0),
BundledChunkExtractor.FACTORY.createProgressiveMediaExtractor( BundledChunkExtractor.FACTORY.createProgressiveMediaExtractor(
trackType, trackType,
representation.format, representation.format,
@ -486,18 +492,45 @@ public class DefaultDashChunkSource implements DashChunkSource {
} }
} }
} }
LoadErrorHandlingPolicy.FallbackOptions fallbackOptions = createFallbackOptions(trackSelection);
if (fallbackOptions.numberOfTracks - fallbackOptions.numberOfExcludedTracks <= 1) { int trackIndex = trackSelection.indexOf(chunk.trackFormat);
// No more alternative tracks remaining. RepresentationHolder representationHolder = representationHolders[trackIndex];
LoadErrorHandlingPolicy.FallbackOptions fallbackOptions =
createFallbackOptions(trackSelection, representationHolder.representation.baseUrls);
if (!fallbackOptions.isFallbackAvailable(LoadErrorHandlingPolicy.FALLBACK_TYPE_TRACK)
&& !fallbackOptions.isFallbackAvailable(LoadErrorHandlingPolicy.FALLBACK_TYPE_LOCATION)) {
// No more alternatives remaining.
return false; return false;
} }
@Nullable @Nullable
LoadErrorHandlingPolicy.FallbackSelection fallbackSelection = LoadErrorHandlingPolicy.FallbackSelection fallbackSelection =
loadErrorHandlingPolicy.getFallbackSelectionFor(fallbackOptions, loadErrorInfo); loadErrorHandlingPolicy.getFallbackSelectionFor(fallbackOptions, loadErrorInfo);
return fallbackSelection != null if (fallbackSelection == null) {
&& fallbackSelection.type == FALLBACK_TYPE_TRACK // Policy indicated to not use any fallback.
&& trackSelection.blacklist( return false;
trackSelection.indexOf(chunk.trackFormat), fallbackSelection.exclusionDurationMs); }
boolean cancelLoad = false;
if (fallbackSelection.type == LoadErrorHandlingPolicy.FALLBACK_TYPE_TRACK) {
cancelLoad =
trackSelection.blacklist(
trackSelection.indexOf(chunk.trackFormat), fallbackSelection.exclusionDurationMs);
} else if (fallbackSelection.type == LoadErrorHandlingPolicy.FALLBACK_TYPE_LOCATION) {
baseUrlExclusionList.exclude(
representationHolder.selectedBaseUrl, fallbackSelection.exclusionDurationMs);
for (int i = 0; i < representationHolders.length; i++) {
@Nullable
BaseUrl baseUrl =
baseUrlExclusionList.selectBaseUrl(representationHolders[i].representation.baseUrls);
if (baseUrl != null) {
if (i == trackIndex) {
cancelLoad = true;
}
representationHolders[i] = representationHolders[i].copyWithNewSelectedBaseUrl(baseUrl);
}
}
}
return cancelLoad;
} }
@Override @Override
@ -512,6 +545,25 @@ public class DefaultDashChunkSource implements DashChunkSource {
// Internal methods. // Internal methods.
private LoadErrorHandlingPolicy.FallbackOptions createFallbackOptions(
ExoTrackSelection trackSelection, List<BaseUrl> baseUrls) {
long nowMs = SystemClock.elapsedRealtime();
int numberOfTracks = trackSelection.length();
int numberOfExcludedTracks = 0;
for (int i = 0; i < numberOfTracks; i++) {
if (trackSelection.isBlacklisted(i, nowMs)) {
numberOfExcludedTracks++;
}
}
int priorityCount = BaseUrlExclusionList.getPriorityCount(baseUrls);
return new LoadErrorHandlingPolicy.FallbackOptions(
/* numberOfLocations= */ priorityCount,
/* numberOfExcludedLocations= */ priorityCount
- baseUrlExclusionList.getPriorityCountAfterExclusion(baseUrls),
numberOfTracks,
numberOfExcludedTracks);
}
private long getSegmentNum( private long getSegmentNum(
RepresentationHolder representationHolder, RepresentationHolder representationHolder,
@Nullable MediaChunk previousChunk, @Nullable MediaChunk previousChunk,
@ -848,6 +900,17 @@ public class DefaultDashChunkSource implements DashChunkSource {
segmentIndex); segmentIndex);
} }
@CheckResult
/* package */ RepresentationHolder copyWithNewSelectedBaseUrl(BaseUrl selectedBaseUrl) {
return new RepresentationHolder(
periodDurationUs,
representation,
selectedBaseUrl,
chunkExtractor,
segmentNumShift,
segmentIndex);
}
public long getFirstSegmentNum() { public long getFirstSegmentNum() {
return segmentIndex.getFirstSegmentNum() + segmentNumShift; return segmentIndex.getFirstSegmentNum() + segmentNumShift;
} }

View File

@ -0,0 +1,247 @@
/*
* Copyright (C) 2021 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 com.google.android.exoplayer2.upstream.DefaultLoadErrorHandlingPolicy.DEFAULT_LOCATION_EXCLUSION_MS;
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.source.dash.manifest.BaseUrl;
import com.google.common.collect.ImmutableList;
import java.time.Duration;
import java.util.List;
import java.util.Random;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.shadows.ShadowSystemClock;
/** Unit tests for {@link BaseUrlExclusionList}. */
@RunWith(AndroidJUnit4.class)
public class BaseUrlExclusionListTest {
@Test
public void selectBaseUrl_excludeByServiceLocation_excludesAllBaseUrlOfSameServiceLocation() {
BaseUrlExclusionList baseUrlExclusionList = new BaseUrlExclusionList();
List<BaseUrl> baseUrls =
ImmutableList.of(
new BaseUrl(
/* url= */ "a", /* serviceLocation= */ "a", /* priority= */ 1, /* weight= */ 1),
new BaseUrl(
/* url= */ "b", /* serviceLocation= */ "a", /* priority= */ 2, /* weight= */ 1),
new BaseUrl(
/* url= */ "c", /* serviceLocation= */ "c", /* priority= */ 3, /* weight= */ 1));
baseUrlExclusionList.exclude(baseUrls.get(0), 5000);
ShadowSystemClock.advanceBy(Duration.ofMillis(4999));
assertThat(baseUrlExclusionList.selectBaseUrl(baseUrls).url).isEqualTo("c");
ShadowSystemClock.advanceBy(Duration.ofMillis(1));
assertThat(baseUrlExclusionList.selectBaseUrl(baseUrls).url).isEqualTo("a");
}
@Test
public void selectBaseUrl_excludeByPriority_excludesAllBaseUrlsOfSamePriority() {
Random mockRandom = mock(Random.class);
when(mockRandom.nextInt(anyInt())).thenReturn(0);
BaseUrlExclusionList baseUrlExclusionList = new BaseUrlExclusionList(mockRandom);
List<BaseUrl> baseUrls =
ImmutableList.of(
new BaseUrl(
/* url= */ "a", /* serviceLocation= */ "a", /* priority= */ 1, /* weight= */ 1),
new BaseUrl(
/* url= */ "b", /* serviceLocation= */ "b", /* priority= */ 1, /* weight= */ 99),
new BaseUrl(
/* url= */ "c", /* serviceLocation= */ "c", /* priority= */ 2, /* weight= */ 1));
baseUrlExclusionList.exclude(baseUrls.get(0), 5000);
ShadowSystemClock.advanceBy(Duration.ofMillis(4999));
assertThat(baseUrlExclusionList.selectBaseUrl(baseUrls).url).isEqualTo("c");
ShadowSystemClock.advanceBy(Duration.ofMillis(1));
assertThat(baseUrlExclusionList.selectBaseUrl(baseUrls).url).isEqualTo("a");
}
@Test
public void selectBaseUrl_samePriority_choiceIsRandom() {
List<BaseUrl> baseUrls =
ImmutableList.of(
new BaseUrl(
/* url= */ "a", /* serviceLocation= */ "a", /* priority= */ 1, /* weight= */ 99),
new BaseUrl(
/* url= */ "b", /* serviceLocation= */ "b", /* priority= */ 1, /* weight= */ 1));
Random mockRandom = mock(Random.class);
when(mockRandom.nextInt(anyInt())).thenReturn(99);
assertThat(new BaseUrlExclusionList(mockRandom).selectBaseUrl(baseUrls))
.isEqualTo(baseUrls.get(1));
// Random is used for random choice.
verify(mockRandom).nextInt(/* bound= */ 100);
verifyNoMoreInteractions(mockRandom);
}
@Test
public void selectBaseUrl_samePriority_choiceFromSameElementsRandomOnlyOnceSameAfterwards() {
List<BaseUrl> baseUrlsVideo =
ImmutableList.of(
new BaseUrl(
/* url= */ "a/v", /* serviceLocation= */ "a", /* priority= */ 1, /* weight= */ 99),
new BaseUrl(
/* url= */ "b/v", /* serviceLocation= */ "b", /* priority= */ 1, /* weight= */ 1));
List<BaseUrl> baseUrlsAudio =
ImmutableList.of(
new BaseUrl(
/* url= */ "a/a", /* serviceLocation= */ "a", /* priority= */ 1, /* weight= */ 99),
new BaseUrl(
/* url= */ "b/a", /* serviceLocation= */ "b", /* priority= */ 1, /* weight= */ 1));
Random mockRandom = mock(Random.class);
BaseUrlExclusionList baseUrlExclusionList = new BaseUrlExclusionList(mockRandom);
when(mockRandom.nextInt(anyInt())).thenReturn(99);
for (int i = 0; i < 5; i++) {
assertThat(baseUrlExclusionList.selectBaseUrl(baseUrlsVideo).serviceLocation).isEqualTo("b");
assertThat(baseUrlExclusionList.selectBaseUrl(baseUrlsAudio).serviceLocation).isEqualTo("b");
}
// Random is used only once.
verify(mockRandom).nextInt(/* bound= */ 100);
verifyNoMoreInteractions(mockRandom);
}
@Test
public void selectBaseUrl_twiceTheSameLocationExcluded_correctExpirationDuration() {
List<BaseUrl> baseUrls =
ImmutableList.of(
new BaseUrl(
/* url= */ "a", /* serviceLocation= */ "a", /* priority= */ 1, /* weight= */ 1),
new BaseUrl(
/* url= */ "c", /* serviceLocation= */ "a", /* priority= */ 2, /* weight= */ 1),
new BaseUrl(
/* url= */ "d", /* serviceLocation= */ "d", /* priority= */ 2, /* weight= */ 1));
BaseUrlExclusionList baseUrlExclusionList = new BaseUrlExclusionList();
// Exclude location 'a'.
baseUrlExclusionList.exclude(baseUrls.get(0), 5000);
// Exclude location 'a' which increases exclusion duration of 'a'.
baseUrlExclusionList.exclude(baseUrls.get(1), 10000);
assertThat(baseUrlExclusionList.selectBaseUrl(baseUrls)).isNull();
ShadowSystemClock.advanceBy(Duration.ofMillis(9999));
// Location 'a' still excluded.
assertThat(baseUrlExclusionList.selectBaseUrl(baseUrls)).isNull();
ShadowSystemClock.advanceBy(Duration.ofMillis(1));
assertThat(baseUrlExclusionList.getPriorityCountAfterExclusion(baseUrls)).isEqualTo(2);
assertThat(baseUrlExclusionList.selectBaseUrl(baseUrls).url).isEqualTo("a");
}
@Test
public void selectBaseUrl_twiceTheSamePriorityExcluded_correctExpirationDuration() {
List<BaseUrl> baseUrls =
ImmutableList.of(
new BaseUrl(
/* url= */ "a", /* serviceLocation= */ "a", /* priority= */ 1, /* weight= */ 1),
new BaseUrl(
/* url= */ "b", /* serviceLocation= */ "b", /* priority= */ 1, /* weight= */ 1),
new BaseUrl(
/* url= */ "c", /* serviceLocation= */ "c", /* priority= */ 2, /* weight= */ 1));
BaseUrlExclusionList baseUrlExclusionList = new BaseUrlExclusionList();
// Exclude priority 1.
baseUrlExclusionList.exclude(baseUrls.get(0), 5000);
// Exclude priority 1 again which increases the exclusion duration.
baseUrlExclusionList.exclude(baseUrls.get(1), 10000);
assertThat(baseUrlExclusionList.selectBaseUrl(baseUrls).url).isEqualTo("c");
ShadowSystemClock.advanceBy(Duration.ofMillis(9999));
assertThat(baseUrlExclusionList.getPriorityCountAfterExclusion(baseUrls)).isEqualTo(1);
ShadowSystemClock.advanceBy(Duration.ofMillis(1));
assertThat(baseUrlExclusionList.getPriorityCountAfterExclusion(baseUrls)).isEqualTo(2);
}
@Test
public void selectBaseUrl_emptyBaseUrlList_selectionIsNull() {
BaseUrlExclusionList baseUrlExclusionList = new BaseUrlExclusionList();
assertThat(baseUrlExclusionList.selectBaseUrl(ImmutableList.of())).isNull();
}
@Test
public void reset_dropsAllExclusions() {
BaseUrlExclusionList baseUrlExclusionList = new BaseUrlExclusionList();
List<BaseUrl> baseUrls = ImmutableList.of(new BaseUrl("a"));
baseUrlExclusionList.exclude(baseUrls.get(0), 5000);
baseUrlExclusionList.reset();
assertThat(baseUrlExclusionList.selectBaseUrl(baseUrls).url).isEqualTo("a");
}
@Test
public void getPriorityCountAfterExclusion_correctPriorityCount() {
List<BaseUrl> baseUrls =
ImmutableList.of(
new BaseUrl(
/* url= */ "a", /* serviceLocation= */ "a", /* priority= */ 1, /* weight= */ 1),
new BaseUrl(
/* url= */ "b", /* serviceLocation= */ "b", /* priority= */ 2, /* weight= */ 1),
new BaseUrl(
/* url= */ "c", /* serviceLocation= */ "c", /* priority= */ 2, /* weight= */ 1),
new BaseUrl(
/* url= */ "d", /* serviceLocation= */ "d", /* priority= */ 3, /* weight= */ 1),
new BaseUrl(
/* url= */ "e", /* serviceLocation= */ "e", /* priority= */ 3, /* weight= */ 1));
BaseUrlExclusionList baseUrlExclusionList = new BaseUrlExclusionList();
// Empty base URL list.
assertThat(baseUrlExclusionList.getPriorityCountAfterExclusion(ImmutableList.of()))
.isEqualTo(0);
assertThat(baseUrlExclusionList.getPriorityCountAfterExclusion(baseUrls)).isEqualTo(3);
// Exclude base urls.
baseUrlExclusionList.exclude(baseUrls.get(0), DEFAULT_LOCATION_EXCLUSION_MS);
assertThat(baseUrlExclusionList.getPriorityCountAfterExclusion(baseUrls)).isEqualTo(2);
baseUrlExclusionList.exclude(baseUrls.get(1), 2 * DEFAULT_LOCATION_EXCLUSION_MS);
assertThat(baseUrlExclusionList.getPriorityCountAfterExclusion(baseUrls)).isEqualTo(1);
baseUrlExclusionList.exclude(baseUrls.get(3), 3 * DEFAULT_LOCATION_EXCLUSION_MS);
assertThat(baseUrlExclusionList.getPriorityCountAfterExclusion(baseUrls)).isEqualTo(0);
// Time passes.
ShadowSystemClock.advanceBy(Duration.ofMillis(DEFAULT_LOCATION_EXCLUSION_MS));
assertThat(baseUrlExclusionList.getPriorityCountAfterExclusion(baseUrls)).isEqualTo(1);
ShadowSystemClock.advanceBy(Duration.ofMillis(DEFAULT_LOCATION_EXCLUSION_MS));
assertThat(baseUrlExclusionList.getPriorityCountAfterExclusion(baseUrls)).isEqualTo(2);
ShadowSystemClock.advanceBy(Duration.ofMillis(DEFAULT_LOCATION_EXCLUSION_MS));
assertThat(baseUrlExclusionList.getPriorityCountAfterExclusion(baseUrls)).isEqualTo(3);
}
@Test
public void getPriorityCount_correctPriorityCount() {
List<BaseUrl> baseUrls =
ImmutableList.of(
new BaseUrl(
/* url= */ "a", /* serviceLocation= */ "a", /* priority= */ 1, /* weight= */ 1),
new BaseUrl(
/* url= */ "b", /* serviceLocation= */ "b", /* priority= */ 2, /* weight= */ 1),
new BaseUrl(
/* url= */ "c", /* serviceLocation= */ "c", /* priority= */ 2, /* weight= */ 1),
new BaseUrl(
/* url= */ "d", /* serviceLocation= */ "d", /* priority= */ 3, /* weight= */ 1));
assertThat(BaseUrlExclusionList.getPriorityCount(baseUrls)).isEqualTo(3);
assertThat(BaseUrlExclusionList.getPriorityCount(ImmutableList.of())).isEqualTo(0);
}
}

View File

@ -200,6 +200,7 @@ public final class DashMediaPeriodTest {
return new DashMediaPeriod( return new DashMediaPeriod(
/* id= */ periodIndex, /* id= */ periodIndex,
manifest, manifest,
new BaseUrlExclusionList(),
periodIndex, periodIndex,
mock(DashChunkSource.Factory.class), mock(DashChunkSource.Factory.class),
mock(TransferListener.class), mock(TransferListener.class),

View File

@ -15,6 +15,7 @@
*/ */
package com.google.android.exoplayer2.source.dash; package com.google.android.exoplayer2.source.dash;
import static com.google.android.exoplayer2.upstream.DefaultLoadErrorHandlingPolicy.DEFAULT_LOCATION_EXCLUSION_MS;
import static com.google.android.exoplayer2.util.Assertions.checkNotNull; import static com.google.android.exoplayer2.util.Assertions.checkNotNull;
import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertThat;
@ -29,6 +30,7 @@ import com.google.android.exoplayer2.source.LoadEventInfo;
import com.google.android.exoplayer2.source.MediaLoadData; import com.google.android.exoplayer2.source.MediaLoadData;
import com.google.android.exoplayer2.source.TrackGroup; import com.google.android.exoplayer2.source.TrackGroup;
import com.google.android.exoplayer2.source.chunk.BundledChunkExtractor; import com.google.android.exoplayer2.source.chunk.BundledChunkExtractor;
import com.google.android.exoplayer2.source.chunk.Chunk;
import com.google.android.exoplayer2.source.chunk.ChunkHolder; import com.google.android.exoplayer2.source.chunk.ChunkHolder;
import com.google.android.exoplayer2.source.dash.manifest.DashManifest; import com.google.android.exoplayer2.source.dash.manifest.DashManifest;
import com.google.android.exoplayer2.source.dash.manifest.DashManifestParser; import com.google.android.exoplayer2.source.dash.manifest.DashManifestParser;
@ -45,10 +47,16 @@ import com.google.android.exoplayer2.upstream.LoaderErrorThrower;
import com.google.android.exoplayer2.util.Assertions; import com.google.android.exoplayer2.util.Assertions;
import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Lists;
import java.io.IOException; import java.io.IOException;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import org.robolectric.annotation.internal.DoNotInstrument; import org.robolectric.annotation.internal.DoNotInstrument;
import org.robolectric.shadows.ShadowSystemClock;
/** Unit test for {@link DefaultDashChunkSource}. */ /** Unit test for {@link DefaultDashChunkSource}. */
@RunWith(AndroidJUnit4.class) @RunWith(AndroidJUnit4.class)
@ -58,6 +66,8 @@ public class DefaultDashChunkSourceTest {
private static final String SAMPLE_MPD_LIVE_WITH_OFFSET_INSIDE_WINDOW = private static final String SAMPLE_MPD_LIVE_WITH_OFFSET_INSIDE_WINDOW =
"media/mpd/sample_mpd_live_with_offset_inside_window"; "media/mpd/sample_mpd_live_with_offset_inside_window";
private static final String SAMPLE_MPD_VOD = "media/mpd/sample_mpd_vod"; private static final String SAMPLE_MPD_VOD = "media/mpd/sample_mpd_vod";
private static final String SAMPLE_MPD_VOD_LOCATION_FALLBACK =
"media/mpd/sample_mpd_vod_location_fallback";
@Test @Test
public void getNextChunk_forLowLatencyManifest_setsCorrectMayNotLoadAtFullNetworkSpeedFlag() public void getNextChunk_forLowLatencyManifest_setsCorrectMayNotLoadAtFullNetworkSpeedFlag()
@ -76,6 +86,7 @@ public class DefaultDashChunkSourceTest {
BundledChunkExtractor.FACTORY, BundledChunkExtractor.FACTORY,
new LoaderErrorThrower.Dummy(), new LoaderErrorThrower.Dummy(),
manifest, manifest,
new BaseUrlExclusionList(),
/* periodIndex= */ 0, /* periodIndex= */ 0,
/* adaptationSetIndices= */ new int[] {0}, /* adaptationSetIndices= */ new int[] {0},
new FixedTrackSelection(new TrackGroup(new Format.Builder().build()), /* track= */ 0), new FixedTrackSelection(new TrackGroup(new Format.Builder().build()), /* track= */ 0),
@ -123,6 +134,7 @@ public class DefaultDashChunkSourceTest {
BundledChunkExtractor.FACTORY, BundledChunkExtractor.FACTORY,
new LoaderErrorThrower.Dummy(), new LoaderErrorThrower.Dummy(),
manifest, manifest,
new BaseUrlExclusionList(),
/* periodIndex= */ 0, /* periodIndex= */ 0,
/* adaptationSetIndices= */ new int[] {0}, /* adaptationSetIndices= */ new int[] {0},
new FixedTrackSelection(new TrackGroup(new Format.Builder().build()), /* track= */ 0), new FixedTrackSelection(new TrackGroup(new Format.Builder().build()), /* track= */ 0),
@ -146,7 +158,63 @@ public class DefaultDashChunkSourceTest {
} }
@Test @Test
public void onChunkLoadError_trackExclusionEnabled_requestReplacementChunkAsLongAsAvailable() public void getNextChunk_onChunkLoadErrorLocationExclusionEnabled_correctFallbackBehavior()
throws Exception {
DefaultLoadErrorHandlingPolicy loadErrorHandlingPolicy =
new DefaultLoadErrorHandlingPolicy() {
@Override
public FallbackSelection getFallbackSelectionFor(
FallbackOptions fallbackOptions, LoadErrorInfo loadErrorInfo) {
return new FallbackSelection(FALLBACK_TYPE_LOCATION, DEFAULT_LOCATION_EXCLUSION_MS);
}
};
List<Chunk> chunks = new ArrayList<>();
DashChunkSource chunkSource = createDashChunkSource(/* numberOfTracks= */ 1);
ChunkHolder output = new ChunkHolder();
boolean requestReplacementChunk = true;
while (requestReplacementChunk) {
chunkSource.getNextChunk(
/* playbackPositionUs= */ 0,
/* loadPositionUs= */ 0,
/* queue= */ ImmutableList.of(),
output);
chunks.add(output.chunk);
requestReplacementChunk =
chunkSource.onChunkLoadError(
checkNotNull(output.chunk),
/* cancelable= */ true,
createFakeLoadErrorInfo(
output.chunk.dataSpec, /* httpResponseCode= */ 404, /* errorCount= */ 1),
loadErrorHandlingPolicy);
}
assertThat(Lists.transform(chunks, (chunk) -> chunk.dataSpec.uri.toString()))
.containsExactly(
"http://video.com/baseUrl/a/video/video_0_1300000.m4s",
"http://video.com/baseUrl/b/video/video_0_1300000.m4s",
"http://video.com/baseUrl/d/video/video_0_1300000.m4s")
.inOrder();
// Assert expiration of exclusions.
ShadowSystemClock.advanceBy(Duration.ofMillis(DEFAULT_LOCATION_EXCLUSION_MS));
chunkSource.onChunkLoadError(
checkNotNull(output.chunk),
/* cancelable= */ true,
createFakeLoadErrorInfo(
output.chunk.dataSpec, /* httpResponseCode= */ 404, /* errorCount= */ 1),
loadErrorHandlingPolicy);
chunkSource.getNextChunk(
/* playbackPositionUs= */ 0,
/* loadPositionUs= */ 0,
/* queue= */ ImmutableList.of(),
output);
assertThat(output.chunk.dataSpec.uri.toString())
.isEqualTo("http://video.com/baseUrl/a/video/video_0_1300000.m4s");
}
@Test
public void getNextChunk_onChunkLoadErrorTrackExclusionEnabled_correctFallbackBehavior()
throws Exception { throws Exception {
DefaultLoadErrorHandlingPolicy loadErrorHandlingPolicy = DefaultLoadErrorHandlingPolicy loadErrorHandlingPolicy =
new DefaultLoadErrorHandlingPolicy() { new DefaultLoadErrorHandlingPolicy() {
@ -158,31 +226,36 @@ public class DefaultDashChunkSourceTest {
FALLBACK_TYPE_TRACK, DefaultLoadErrorHandlingPolicy.DEFAULT_TRACK_EXCLUSION_MS); FALLBACK_TYPE_TRACK, DefaultLoadErrorHandlingPolicy.DEFAULT_TRACK_EXCLUSION_MS);
} }
}; };
int numberOfTracks = 2; DashChunkSource chunkSource = createDashChunkSource(/* numberOfTracks= */ 4);
DashChunkSource chunkSource = createDashChunkSource(numberOfTracks);
ChunkHolder output = new ChunkHolder(); ChunkHolder output = new ChunkHolder();
for (int i = 0; i < numberOfTracks; i++) { List<Chunk> chunks = new ArrayList<>();
boolean requestReplacementChunk = true;
while (requestReplacementChunk) {
chunkSource.getNextChunk( chunkSource.getNextChunk(
/* playbackPositionUs= */ 0, /* playbackPositionUs= */ 0,
/* loadPositionUs= */ 0, /* loadPositionUs= */ 0,
/* queue= */ ImmutableList.of(), /* queue= */ ImmutableList.of(),
output); output);
chunks.add(output.chunk);
boolean alternativeTrackAvailable = requestReplacementChunk =
chunkSource.onChunkLoadError( chunkSource.onChunkLoadError(
checkNotNull(output.chunk), checkNotNull(output.chunk),
/* cancelable= */ true, /* cancelable= */ true,
createFakeLoadErrorInfo( createFakeLoadErrorInfo(
output.chunk.dataSpec, /* httpResponseCode= */ 404, /* errorCount= */ 1), output.chunk.dataSpec, /* httpResponseCode= */ 404, /* errorCount= */ 1),
loadErrorHandlingPolicy); loadErrorHandlingPolicy);
// Expect true except for the last track remaining.
assertThat(alternativeTrackAvailable).isEqualTo(i != numberOfTracks - 1);
} }
assertThat(Lists.transform(chunks, (chunk) -> chunk.dataSpec.uri.toString()))
.containsExactly(
"http://video.com/baseUrl/a/video/video_0_700000.m4s",
"http://video.com/baseUrl/a/video/video_0_452000.m4s",
"http://video.com/baseUrl/a/video/video_0_250000.m4s",
"http://video.com/baseUrl/a/video/video_0_1300000.m4s")
.inOrder();
} }
@Test @Test
public void onChunkLoadError_trackExclusionDisabled_neverRequestReplacementChunk() public void getNextChunk_onChunkLoadErrorExclusionDisabled_neverRequestReplacementChunk()
throws Exception { throws Exception {
DefaultLoadErrorHandlingPolicy loadErrorHandlingPolicy = DefaultLoadErrorHandlingPolicy loadErrorHandlingPolicy =
new DefaultLoadErrorHandlingPolicy() { new DefaultLoadErrorHandlingPolicy() {
@ -202,7 +275,7 @@ public class DefaultDashChunkSourceTest {
/* queue= */ ImmutableList.of(), /* queue= */ ImmutableList.of(),
output); output);
boolean alternativeTrackAvailable = boolean requestReplacementChunk =
chunkSource.onChunkLoadError( chunkSource.onChunkLoadError(
checkNotNull(output.chunk), checkNotNull(output.chunk),
/* cancelable= */ true, /* cancelable= */ true,
@ -210,7 +283,7 @@ public class DefaultDashChunkSourceTest {
output.chunk.dataSpec, /* httpResponseCode= */ 404, /* errorCount= */ 1), output.chunk.dataSpec, /* httpResponseCode= */ 404, /* errorCount= */ 1),
loadErrorHandlingPolicy); loadErrorHandlingPolicy);
assertThat(alternativeTrackAvailable).isFalse(); assertThat(requestReplacementChunk).isFalse();
} }
private DashChunkSource createDashChunkSource(int numberOfTracks) throws IOException { private DashChunkSource createDashChunkSource(int numberOfTracks) throws IOException {
@ -220,7 +293,7 @@ public class DefaultDashChunkSourceTest {
.parse( .parse(
Uri.parse("https://example.com/test.mpd"), Uri.parse("https://example.com/test.mpd"),
TestUtil.getInputStream( TestUtil.getInputStream(
ApplicationProvider.getApplicationContext(), SAMPLE_MPD_VOD)); ApplicationProvider.getApplicationContext(), SAMPLE_MPD_VOD_LOCATION_FALLBACK));
int[] adaptationSetIndices = new int[] {0}; int[] adaptationSetIndices = new int[] {0};
int[] selectedTracks = new int[numberOfTracks]; int[] selectedTracks = new int[numberOfTracks];
Format[] formats = new Format[numberOfTracks]; Format[] formats = new Format[numberOfTracks];
@ -244,6 +317,7 @@ public class DefaultDashChunkSourceTest {
BundledChunkExtractor.FACTORY, BundledChunkExtractor.FACTORY,
new LoaderErrorThrower.Dummy(), new LoaderErrorThrower.Dummy(),
manifest, manifest,
new BaseUrlExclusionList(new Random(/* seed= */ 1234)),
/* periodIndex= */ 0, /* periodIndex= */ 0,
/* adaptationSetIndices= */ adaptationSetIndices, /* adaptationSetIndices= */ adaptationSetIndices,
adaptiveTrackSelection, adaptiveTrackSelection,

View File

@ -0,0 +1,41 @@
<?xml version="1.0" encoding="UTF-8"?>
<MPD xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="urn:mpeg:dash:schema:mpd:2011"
xsi:schemaLocation="urn:mpeg:dash:schema:mpd:2011"
xmlns:dvb="urn:dvb:dash:dash-extensions:2014-1"
minBufferTime="PT1S"
profiles="urn:mpeg:dash:profile:isoff-main:2011"
type="static"
mediaPresentationDuration="PT904S">
<BaseURL serviceLocation="a" dvb:priority="1" dvb:weight="1">http://video.com/baseUrl/a/</BaseURL>
<BaseURL serviceLocation="b" dvb:priority="2" dvb:weight="1">http://video.com/baseUrl/b/</BaseURL>
<BaseURL serviceLocation="c" dvb:priority="2" dvb:weight="1">http://video.com/baseUrl/b/</BaseURL>
<BaseURL serviceLocation="d" dvb:priority="3" dvb:weight="1">http://video.com/baseUrl/d/</BaseURL>
<Period id="1" duration="PT904S" start="PT0S">
<AdaptationSet id="0" mimeType="video/mp4" contentType="video" segmentAlignment="true" startWithSAP="1">
<Role schemeIdUri="urn:mpeg:dash:role:2011" value="main"/>
<BaseURL>video/</BaseURL>
<SegmentTemplate presentationTimeOffset="0" media="video_$Time$_$Bandwidth$.m4s" timescale="1000" >
<SegmentTimeline>
<S d="4000" r="225"/>
</SegmentTimeline>
</SegmentTemplate>
<Representation id="0" codecs="avc1.4d401e" width="768" height="432" frameRate="25" bandwidth="1300000"/>
<Representation id="1" codecs="avc1.4d4015" width="512" height="288" frameRate="25" bandwidth="700000"/>
<Representation id="2" codecs="avc1.4d4015" width="512" height="288" frameRate="25" bandwidth="452000"/>
<Representation id="3" codecs="avc1.42c00c" width="400" height="224" frameRate="25/2" bandwidth="250000"/>
<Representation id="4" codecs="avc1.42c00b" width="400" height="224" frameRate="5" bandwidth="50000"/>
</AdaptationSet>
<AdaptationSet id="1" lang="fr" mimeType="audio/mp4" contentType="audio" codecs="mp4a.40.2" segmentAlignment="true" startWithSAP="1">
<BaseURL>audio/</BaseURL>
<AudioChannelConfiguration schemeIdUri="urn:mpeg:mpegB:cicp:ChannelConfiguration" value="2"/>
<SegmentTemplate presentationTimeOffset="0" media="audio_$Time$_$Bandwidth$.m4s" timescale="1000" >
<SegmentTimeline>
<S d="3200" r="281"/>
<S d="1600"/>
</SegmentTimeline>
</SegmentTemplate>
<Representation id="5" bandwidth="128000"/>
</AdaptationSet>
</Period>
</MPD>