Add BaseUrlExlusionList and use it to select base URLs
Issues: #771 and #7654 PiperOrigin-RevId: 386850707
This commit is contained in:
parent
c6e860b8bb
commit
3f9093cc02
@ -47,6 +47,9 @@
|
||||
`TrackSelection#getFormat()`.
|
||||
* Deprecate `ControlDispatcher` and `DefaultControlDispatcher`. Use a
|
||||
`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 `Player.getPlaybackError`. Use `Player.getPlayerError` instead.
|
||||
* Remove `Player.getCurrentTag`. Use `Player.getCurrentMediaItem` and
|
||||
@ -160,6 +163,12 @@
|
||||
([#9158](https://github.com/google/ExoPlayer/issues/9158)).
|
||||
* Fix issue around TS synchronization when reading a file's duration
|
||||
([#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:
|
||||
* Fix issue where playback of a live event could become stuck rather than
|
||||
transitioning to `STATE_ENDED` when the event ends
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
@ -35,6 +35,7 @@ public interface DashChunkSource extends ChunkSource {
|
||||
/**
|
||||
* @param manifestLoaderErrorThrower Throws errors affecting loading of manifests.
|
||||
* @param manifest The initial manifest.
|
||||
* @param baseUrlExclusionList The base URL exclusion list.
|
||||
* @param periodIndex The index of the corresponding period in the manifest.
|
||||
* @param adaptationSetIndices The indices of the corresponding adaptation sets in the period.
|
||||
* @param trackSelection The track selection.
|
||||
@ -51,6 +52,7 @@ public interface DashChunkSource extends ChunkSource {
|
||||
DashChunkSource createDashChunkSource(
|
||||
LoaderErrorThrower manifestLoaderErrorThrower,
|
||||
DashManifest manifest,
|
||||
BaseUrlExclusionList baseUrlExclusionList,
|
||||
int periodIndex,
|
||||
int[] adaptationSetIndices,
|
||||
ExoTrackSelection trackSelection,
|
||||
|
@ -84,6 +84,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
|
||||
@Nullable private final TransferListener transferListener;
|
||||
private final DrmSessionManager drmSessionManager;
|
||||
private final LoadErrorHandlingPolicy loadErrorHandlingPolicy;
|
||||
private final BaseUrlExclusionList baseUrlExclusionList;
|
||||
private final long elapsedRealtimeOffsetMs;
|
||||
private final LoaderErrorThrower manifestLoaderErrorThrower;
|
||||
private final Allocator allocator;
|
||||
@ -107,6 +108,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
|
||||
public DashMediaPeriod(
|
||||
int id,
|
||||
DashManifest manifest,
|
||||
BaseUrlExclusionList baseUrlExclusionList,
|
||||
int periodIndex,
|
||||
DashChunkSource.Factory chunkSourceFactory,
|
||||
@Nullable TransferListener transferListener,
|
||||
@ -121,6 +123,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
|
||||
PlayerEmsgCallback playerEmsgCallback) {
|
||||
this.id = id;
|
||||
this.manifest = manifest;
|
||||
this.baseUrlExclusionList = baseUrlExclusionList;
|
||||
this.periodIndex = periodIndex;
|
||||
this.chunkSourceFactory = chunkSourceFactory;
|
||||
this.transferListener = transferListener;
|
||||
@ -766,6 +769,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType;
|
||||
chunkSourceFactory.createDashChunkSource(
|
||||
manifestLoaderErrorThrower,
|
||||
manifest,
|
||||
baseUrlExclusionList,
|
||||
periodIndex,
|
||||
trackGroupInfo.adaptationSetIndices,
|
||||
selection,
|
||||
|
@ -448,6 +448,7 @@ public final class DashMediaSource extends BaseMediaSource {
|
||||
private final CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory;
|
||||
private final DrmSessionManager drmSessionManager;
|
||||
private final LoadErrorHandlingPolicy loadErrorHandlingPolicy;
|
||||
private final BaseUrlExclusionList baseUrlExclusionList;
|
||||
private final long fallbackTargetLiveOffsetMs;
|
||||
private final EventDispatcher manifestEventDispatcher;
|
||||
private final ParsingLoadable.Parser<? extends DashManifest> manifestParser;
|
||||
@ -502,6 +503,7 @@ public final class DashMediaSource extends BaseMediaSource {
|
||||
this.loadErrorHandlingPolicy = loadErrorHandlingPolicy;
|
||||
this.fallbackTargetLiveOffsetMs = fallbackTargetLiveOffsetMs;
|
||||
this.compositeSequenceableLoaderFactory = compositeSequenceableLoaderFactory;
|
||||
baseUrlExclusionList = new BaseUrlExclusionList();
|
||||
sideloadedManifest = manifest != null;
|
||||
manifestEventDispatcher = createEventDispatcher(/* mediaPeriodId= */ null);
|
||||
manifestUriLock = new Object();
|
||||
@ -572,6 +574,7 @@ public final class DashMediaSource extends BaseMediaSource {
|
||||
new DashMediaPeriod(
|
||||
firstPeriodId + periodIndex,
|
||||
manifest,
|
||||
baseUrlExclusionList,
|
||||
periodIndex,
|
||||
chunkSourceFactory,
|
||||
mediaTransferListener,
|
||||
@ -617,6 +620,7 @@ public final class DashMediaSource extends BaseMediaSource {
|
||||
expiredManifestPublishTimeUs = C.TIME_UNSET;
|
||||
firstPeriodId = 0;
|
||||
periodsById.clear();
|
||||
baseUrlExclusionList.reset();
|
||||
drmSessionManager.release();
|
||||
}
|
||||
|
||||
|
@ -15,8 +15,6 @@
|
||||
*/
|
||||
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.min;
|
||||
|
||||
@ -103,6 +101,7 @@ public class DefaultDashChunkSource implements DashChunkSource {
|
||||
public DashChunkSource createDashChunkSource(
|
||||
LoaderErrorThrower manifestLoaderErrorThrower,
|
||||
DashManifest manifest,
|
||||
BaseUrlExclusionList baseUrlExclusionList,
|
||||
int periodIndex,
|
||||
int[] adaptationSetIndices,
|
||||
ExoTrackSelection trackSelection,
|
||||
@ -120,6 +119,7 @@ public class DefaultDashChunkSource implements DashChunkSource {
|
||||
chunkExtractorFactory,
|
||||
manifestLoaderErrorThrower,
|
||||
manifest,
|
||||
baseUrlExclusionList,
|
||||
periodIndex,
|
||||
adaptationSetIndices,
|
||||
trackSelection,
|
||||
@ -134,6 +134,7 @@ public class DefaultDashChunkSource implements DashChunkSource {
|
||||
}
|
||||
|
||||
private final LoaderErrorThrower manifestLoaderErrorThrower;
|
||||
private final BaseUrlExclusionList baseUrlExclusionList;
|
||||
private final int[] adaptationSetIndices;
|
||||
private final int trackType;
|
||||
private final DataSource dataSource;
|
||||
@ -154,6 +155,7 @@ public class DefaultDashChunkSource implements DashChunkSource {
|
||||
* chunks.
|
||||
* @param manifestLoaderErrorThrower Throws errors affecting loading of manifests.
|
||||
* @param manifest The initial manifest.
|
||||
* @param baseUrlExclusionList The base URL exclusion list.
|
||||
* @param periodIndex The index of the period in the manifest.
|
||||
* @param adaptationSetIndices The indices of the adaptation sets in the period.
|
||||
* @param trackSelection The track selection.
|
||||
@ -174,6 +176,7 @@ public class DefaultDashChunkSource implements DashChunkSource {
|
||||
ChunkExtractor.Factory chunkExtractorFactory,
|
||||
LoaderErrorThrower manifestLoaderErrorThrower,
|
||||
DashManifest manifest,
|
||||
BaseUrlExclusionList baseUrlExclusionList,
|
||||
int periodIndex,
|
||||
int[] adaptationSetIndices,
|
||||
ExoTrackSelection trackSelection,
|
||||
@ -186,6 +189,7 @@ public class DefaultDashChunkSource implements DashChunkSource {
|
||||
@Nullable PlayerTrackEmsgHandler playerTrackEmsgHandler) {
|
||||
this.manifestLoaderErrorThrower = manifestLoaderErrorThrower;
|
||||
this.manifest = manifest;
|
||||
this.baseUrlExclusionList = baseUrlExclusionList;
|
||||
this.adaptationSetIndices = adaptationSetIndices;
|
||||
this.trackSelection = trackSelection;
|
||||
this.trackType = trackType;
|
||||
@ -201,11 +205,13 @@ public class DefaultDashChunkSource implements DashChunkSource {
|
||||
representationHolders = new RepresentationHolder[trackSelection.length()];
|
||||
for (int i = 0; i < representationHolders.length; i++) {
|
||||
Representation representation = representations.get(trackSelection.getIndexInTrackGroup(i));
|
||||
@Nullable
|
||||
BaseUrl selectedBaseUrl = baseUrlExclusionList.selectBaseUrl(representation.baseUrls);
|
||||
representationHolders[i] =
|
||||
new RepresentationHolder(
|
||||
periodDurationUs,
|
||||
representation,
|
||||
representation.baseUrls.get(0),
|
||||
selectedBaseUrl != null ? selectedBaseUrl : representation.baseUrls.get(0),
|
||||
BundledChunkExtractor.FACTORY.createProgressiveMediaExtractor(
|
||||
trackType,
|
||||
representation.format,
|
||||
@ -486,18 +492,45 @@ public class DefaultDashChunkSource implements DashChunkSource {
|
||||
}
|
||||
}
|
||||
}
|
||||
LoadErrorHandlingPolicy.FallbackOptions fallbackOptions = createFallbackOptions(trackSelection);
|
||||
if (fallbackOptions.numberOfTracks - fallbackOptions.numberOfExcludedTracks <= 1) {
|
||||
// No more alternative tracks remaining.
|
||||
|
||||
int trackIndex = trackSelection.indexOf(chunk.trackFormat);
|
||||
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;
|
||||
}
|
||||
@Nullable
|
||||
LoadErrorHandlingPolicy.FallbackSelection fallbackSelection =
|
||||
loadErrorHandlingPolicy.getFallbackSelectionFor(fallbackOptions, loadErrorInfo);
|
||||
return fallbackSelection != null
|
||||
&& fallbackSelection.type == FALLBACK_TYPE_TRACK
|
||||
&& trackSelection.blacklist(
|
||||
trackSelection.indexOf(chunk.trackFormat), fallbackSelection.exclusionDurationMs);
|
||||
if (fallbackSelection == null) {
|
||||
// Policy indicated to not use any fallback.
|
||||
return false;
|
||||
}
|
||||
|
||||
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
|
||||
@ -512,6 +545,25 @@ public class DefaultDashChunkSource implements DashChunkSource {
|
||||
|
||||
// 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(
|
||||
RepresentationHolder representationHolder,
|
||||
@Nullable MediaChunk previousChunk,
|
||||
@ -848,6 +900,17 @@ public class DefaultDashChunkSource implements DashChunkSource {
|
||||
segmentIndex);
|
||||
}
|
||||
|
||||
@CheckResult
|
||||
/* package */ RepresentationHolder copyWithNewSelectedBaseUrl(BaseUrl selectedBaseUrl) {
|
||||
return new RepresentationHolder(
|
||||
periodDurationUs,
|
||||
representation,
|
||||
selectedBaseUrl,
|
||||
chunkExtractor,
|
||||
segmentNumShift,
|
||||
segmentIndex);
|
||||
}
|
||||
|
||||
public long getFirstSegmentNum() {
|
||||
return segmentIndex.getFirstSegmentNum() + segmentNumShift;
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
@ -200,6 +200,7 @@ public final class DashMediaPeriodTest {
|
||||
return new DashMediaPeriod(
|
||||
/* id= */ periodIndex,
|
||||
manifest,
|
||||
new BaseUrlExclusionList(),
|
||||
periodIndex,
|
||||
mock(DashChunkSource.Factory.class),
|
||||
mock(TransferListener.class),
|
||||
|
@ -15,6 +15,7 @@
|
||||
*/
|
||||
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.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.TrackGroup;
|
||||
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.dash.manifest.DashManifest;
|
||||
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.common.collect.ImmutableList;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import com.google.common.collect.Lists;
|
||||
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.runner.RunWith;
|
||||
import org.robolectric.annotation.internal.DoNotInstrument;
|
||||
import org.robolectric.shadows.ShadowSystemClock;
|
||||
|
||||
/** Unit test for {@link DefaultDashChunkSource}. */
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
@ -58,6 +66,8 @@ public class DefaultDashChunkSourceTest {
|
||||
private static final String 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_LOCATION_FALLBACK =
|
||||
"media/mpd/sample_mpd_vod_location_fallback";
|
||||
|
||||
@Test
|
||||
public void getNextChunk_forLowLatencyManifest_setsCorrectMayNotLoadAtFullNetworkSpeedFlag()
|
||||
@ -76,6 +86,7 @@ public class DefaultDashChunkSourceTest {
|
||||
BundledChunkExtractor.FACTORY,
|
||||
new LoaderErrorThrower.Dummy(),
|
||||
manifest,
|
||||
new BaseUrlExclusionList(),
|
||||
/* periodIndex= */ 0,
|
||||
/* adaptationSetIndices= */ new int[] {0},
|
||||
new FixedTrackSelection(new TrackGroup(new Format.Builder().build()), /* track= */ 0),
|
||||
@ -123,6 +134,7 @@ public class DefaultDashChunkSourceTest {
|
||||
BundledChunkExtractor.FACTORY,
|
||||
new LoaderErrorThrower.Dummy(),
|
||||
manifest,
|
||||
new BaseUrlExclusionList(),
|
||||
/* periodIndex= */ 0,
|
||||
/* adaptationSetIndices= */ new int[] {0},
|
||||
new FixedTrackSelection(new TrackGroup(new Format.Builder().build()), /* track= */ 0),
|
||||
@ -146,7 +158,63 @@ public class DefaultDashChunkSourceTest {
|
||||
}
|
||||
|
||||
@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 {
|
||||
DefaultLoadErrorHandlingPolicy loadErrorHandlingPolicy =
|
||||
new DefaultLoadErrorHandlingPolicy() {
|
||||
@ -158,31 +226,36 @@ public class DefaultDashChunkSourceTest {
|
||||
FALLBACK_TYPE_TRACK, DefaultLoadErrorHandlingPolicy.DEFAULT_TRACK_EXCLUSION_MS);
|
||||
}
|
||||
};
|
||||
int numberOfTracks = 2;
|
||||
DashChunkSource chunkSource = createDashChunkSource(numberOfTracks);
|
||||
DashChunkSource chunkSource = createDashChunkSource(/* numberOfTracks= */ 4);
|
||||
ChunkHolder output = new ChunkHolder();
|
||||
for (int i = 0; i < numberOfTracks; i++) {
|
||||
List<Chunk> chunks = new ArrayList<>();
|
||||
boolean requestReplacementChunk = true;
|
||||
while (requestReplacementChunk) {
|
||||
chunkSource.getNextChunk(
|
||||
/* playbackPositionUs= */ 0,
|
||||
/* loadPositionUs= */ 0,
|
||||
/* queue= */ ImmutableList.of(),
|
||||
output);
|
||||
|
||||
boolean alternativeTrackAvailable =
|
||||
chunks.add(output.chunk);
|
||||
requestReplacementChunk =
|
||||
chunkSource.onChunkLoadError(
|
||||
checkNotNull(output.chunk),
|
||||
/* cancelable= */ true,
|
||||
createFakeLoadErrorInfo(
|
||||
output.chunk.dataSpec, /* httpResponseCode= */ 404, /* errorCount= */ 1),
|
||||
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
|
||||
public void onChunkLoadError_trackExclusionDisabled_neverRequestReplacementChunk()
|
||||
public void getNextChunk_onChunkLoadErrorExclusionDisabled_neverRequestReplacementChunk()
|
||||
throws Exception {
|
||||
DefaultLoadErrorHandlingPolicy loadErrorHandlingPolicy =
|
||||
new DefaultLoadErrorHandlingPolicy() {
|
||||
@ -202,7 +275,7 @@ public class DefaultDashChunkSourceTest {
|
||||
/* queue= */ ImmutableList.of(),
|
||||
output);
|
||||
|
||||
boolean alternativeTrackAvailable =
|
||||
boolean requestReplacementChunk =
|
||||
chunkSource.onChunkLoadError(
|
||||
checkNotNull(output.chunk),
|
||||
/* cancelable= */ true,
|
||||
@ -210,7 +283,7 @@ public class DefaultDashChunkSourceTest {
|
||||
output.chunk.dataSpec, /* httpResponseCode= */ 404, /* errorCount= */ 1),
|
||||
loadErrorHandlingPolicy);
|
||||
|
||||
assertThat(alternativeTrackAvailable).isFalse();
|
||||
assertThat(requestReplacementChunk).isFalse();
|
||||
}
|
||||
|
||||
private DashChunkSource createDashChunkSource(int numberOfTracks) throws IOException {
|
||||
@ -220,7 +293,7 @@ public class DefaultDashChunkSourceTest {
|
||||
.parse(
|
||||
Uri.parse("https://example.com/test.mpd"),
|
||||
TestUtil.getInputStream(
|
||||
ApplicationProvider.getApplicationContext(), SAMPLE_MPD_VOD));
|
||||
ApplicationProvider.getApplicationContext(), SAMPLE_MPD_VOD_LOCATION_FALLBACK));
|
||||
int[] adaptationSetIndices = new int[] {0};
|
||||
int[] selectedTracks = new int[numberOfTracks];
|
||||
Format[] formats = new Format[numberOfTracks];
|
||||
@ -244,6 +317,7 @@ public class DefaultDashChunkSourceTest {
|
||||
BundledChunkExtractor.FACTORY,
|
||||
new LoaderErrorThrower.Dummy(),
|
||||
manifest,
|
||||
new BaseUrlExclusionList(new Random(/* seed= */ 1234)),
|
||||
/* periodIndex= */ 0,
|
||||
/* adaptationSetIndices= */ adaptationSetIndices,
|
||||
adaptiveTrackSelection,
|
||||
|
41
testdata/src/test/assets/media/mpd/sample_mpd_vod_location_fallback
vendored
Normal file
41
testdata/src/test/assets/media/mpd/sample_mpd_vod_location_fallback
vendored
Normal 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>
|
Loading…
x
Reference in New Issue
Block a user