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()`.
|
`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
|
||||||
|
@ -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 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,
|
||||||
|
@ -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,
|
||||||
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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(
|
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),
|
||||||
|
@ -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,
|
||||||
|
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