mirror of
https://github.com/androidx/media.git
synced 2025-05-16 03:59:54 +08:00
Use low latency properties in DashMediaSource
Issue: #4904 PiperOrigin-RevId: 337046645
This commit is contained in:
parent
6f66e7d0ba
commit
41b58d503a
@ -16,6 +16,7 @@
|
|||||||
package com.google.android.exoplayer2.source.dash;
|
package com.google.android.exoplayer2.source.dash;
|
||||||
|
|
||||||
import static com.google.android.exoplayer2.util.Assertions.checkNotNull;
|
import static com.google.android.exoplayer2.util.Assertions.checkNotNull;
|
||||||
|
import static com.google.android.exoplayer2.util.Util.castNonNull;
|
||||||
import static java.lang.Math.max;
|
import static java.lang.Math.max;
|
||||||
import static java.lang.Math.min;
|
import static java.lang.Math.min;
|
||||||
|
|
||||||
@ -29,6 +30,7 @@ import com.google.android.exoplayer2.C;
|
|||||||
import com.google.android.exoplayer2.ExoPlayerLibraryInfo;
|
import com.google.android.exoplayer2.ExoPlayerLibraryInfo;
|
||||||
import com.google.android.exoplayer2.MediaItem;
|
import com.google.android.exoplayer2.MediaItem;
|
||||||
import com.google.android.exoplayer2.ParserException;
|
import com.google.android.exoplayer2.ParserException;
|
||||||
|
import com.google.android.exoplayer2.Player;
|
||||||
import com.google.android.exoplayer2.Timeline;
|
import com.google.android.exoplayer2.Timeline;
|
||||||
import com.google.android.exoplayer2.drm.DrmSessionEventListener;
|
import com.google.android.exoplayer2.drm.DrmSessionEventListener;
|
||||||
import com.google.android.exoplayer2.drm.DrmSessionManager;
|
import com.google.android.exoplayer2.drm.DrmSessionManager;
|
||||||
@ -102,8 +104,8 @@ public final class DashMediaSource extends BaseMediaSource {
|
|||||||
@Nullable private DrmSessionManager drmSessionManager;
|
@Nullable private DrmSessionManager drmSessionManager;
|
||||||
private CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory;
|
private CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory;
|
||||||
private LoadErrorHandlingPolicy loadErrorHandlingPolicy;
|
private LoadErrorHandlingPolicy loadErrorHandlingPolicy;
|
||||||
private long livePresentationDelayMs;
|
private long targetLiveOffsetOverrideMs;
|
||||||
private boolean livePresentationDelayOverridesManifest;
|
private long fallbackTargetLiveOffsetMs;
|
||||||
@Nullable private ParsingLoadable.Parser<? extends DashManifest> manifestParser;
|
@Nullable private ParsingLoadable.Parser<? extends DashManifest> manifestParser;
|
||||||
private List<StreamKey> streamKeys;
|
private List<StreamKey> streamKeys;
|
||||||
@Nullable private Object tag;
|
@Nullable private Object tag;
|
||||||
@ -134,7 +136,8 @@ public final class DashMediaSource extends BaseMediaSource {
|
|||||||
this.manifestDataSourceFactory = manifestDataSourceFactory;
|
this.manifestDataSourceFactory = manifestDataSourceFactory;
|
||||||
mediaSourceDrmHelper = new MediaSourceDrmHelper();
|
mediaSourceDrmHelper = new MediaSourceDrmHelper();
|
||||||
loadErrorHandlingPolicy = new DefaultLoadErrorHandlingPolicy();
|
loadErrorHandlingPolicy = new DefaultLoadErrorHandlingPolicy();
|
||||||
livePresentationDelayMs = DEFAULT_LIVE_PRESENTATION_DELAY_MS;
|
targetLiveOffsetOverrideMs = C.TIME_UNSET;
|
||||||
|
fallbackTargetLiveOffsetMs = DEFAULT_FALLBACK_TARGET_LIVE_OFFSET_MS;
|
||||||
compositeSequenceableLoaderFactory = new DefaultCompositeSequenceableLoaderFactory();
|
compositeSequenceableLoaderFactory = new DefaultCompositeSequenceableLoaderFactory();
|
||||||
streamKeys = Collections.emptyList();
|
streamKeys = Collections.emptyList();
|
||||||
}
|
}
|
||||||
@ -204,34 +207,31 @@ public final class DashMediaSource extends BaseMediaSource {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @deprecated Use {@link #setLivePresentationDelayMs(long, boolean)} instead. */
|
/**
|
||||||
|
* @deprecated Use {@link MediaItem.Builder#setLiveTargetOffsetMs(long)} to override the
|
||||||
|
* manifest, or {@link #setFallbackTargetLiveOffsetMs(long)} to provide a fallback value.
|
||||||
|
*/
|
||||||
@Deprecated
|
@Deprecated
|
||||||
@SuppressWarnings("deprecation")
|
public Factory setLivePresentationDelayMs(
|
||||||
public Factory setLivePresentationDelayMs(long livePresentationDelayMs) {
|
long livePresentationDelayMs, boolean overridesManifest) {
|
||||||
if (livePresentationDelayMs == DEFAULT_LIVE_PRESENTATION_DELAY_PREFER_MANIFEST_MS) {
|
targetLiveOffsetOverrideMs = overridesManifest ? livePresentationDelayMs : C.TIME_UNSET;
|
||||||
return setLivePresentationDelayMs(DEFAULT_LIVE_PRESENTATION_DELAY_MS, false);
|
if (!overridesManifest) {
|
||||||
} else {
|
setFallbackTargetLiveOffsetMs(livePresentationDelayMs);
|
||||||
return setLivePresentationDelayMs(livePresentationDelayMs, true);
|
|
||||||
}
|
}
|
||||||
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the duration in milliseconds by which the default start position should precede the end
|
* Sets the {@link Player#getCurrentLiveOffset() target offset for live streams} that is used if
|
||||||
* of the live window for live playbacks. The {@code overridesManifest} parameter specifies
|
* no value is defined in the {@link MediaItem} or the manifest.
|
||||||
* whether the value is used in preference to one in the manifest, if present. The default value
|
|
||||||
* is {@link #DEFAULT_LIVE_PRESENTATION_DELAY_MS}, and by default {@code overridesManifest} is
|
|
||||||
* false.
|
|
||||||
*
|
*
|
||||||
* @param livePresentationDelayMs For live playbacks, the duration in milliseconds by which the
|
* <p>The default value is {@link #DEFAULT_FALLBACK_TARGET_LIVE_OFFSET_MS}.
|
||||||
* default start position should precede the end of the live window.
|
*
|
||||||
* @param overridesManifest Whether the value is used in preference to one in the manifest, if
|
* @param fallbackTargetLiveOffsetMs The fallback live target offset in milliseconds.
|
||||||
* present.
|
|
||||||
* @return This factory, for convenience.
|
* @return This factory, for convenience.
|
||||||
*/
|
*/
|
||||||
public Factory setLivePresentationDelayMs(
|
public Factory setFallbackTargetLiveOffsetMs(long fallbackTargetLiveOffsetMs) {
|
||||||
long livePresentationDelayMs, boolean overridesManifest) {
|
this.fallbackTargetLiveOffsetMs = fallbackTargetLiveOffsetMs;
|
||||||
this.livePresentationDelayMs = livePresentationDelayMs;
|
|
||||||
this.livePresentationDelayOverridesManifest = overridesManifest;
|
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -306,12 +306,17 @@ public final class DashMediaSource extends BaseMediaSource {
|
|||||||
}
|
}
|
||||||
boolean hasUri = mediaItem.playbackProperties != null;
|
boolean hasUri = mediaItem.playbackProperties != null;
|
||||||
boolean hasTag = hasUri && mediaItem.playbackProperties.tag != null;
|
boolean hasTag = hasUri && mediaItem.playbackProperties.tag != null;
|
||||||
|
boolean hasTargetLiveOffset = mediaItem.liveConfiguration.targetLiveOffsetMs != C.TIME_UNSET;
|
||||||
mediaItem =
|
mediaItem =
|
||||||
mediaItem
|
mediaItem
|
||||||
.buildUpon()
|
.buildUpon()
|
||||||
.setMimeType(MimeTypes.APPLICATION_MPD)
|
.setMimeType(MimeTypes.APPLICATION_MPD)
|
||||||
.setUri(hasUri ? mediaItem.playbackProperties.uri : Uri.EMPTY)
|
.setUri(hasUri ? mediaItem.playbackProperties.uri : Uri.EMPTY)
|
||||||
.setTag(hasTag ? mediaItem.playbackProperties.tag : tag)
|
.setTag(hasTag ? mediaItem.playbackProperties.tag : tag)
|
||||||
|
.setLiveTargetOffsetMs(
|
||||||
|
hasTargetLiveOffset
|
||||||
|
? mediaItem.liveConfiguration.targetLiveOffsetMs
|
||||||
|
: targetLiveOffsetOverrideMs)
|
||||||
.setStreamKeys(streamKeys)
|
.setStreamKeys(streamKeys)
|
||||||
.build();
|
.build();
|
||||||
return new DashMediaSource(
|
return new DashMediaSource(
|
||||||
@ -323,8 +328,7 @@ public final class DashMediaSource extends BaseMediaSource {
|
|||||||
compositeSequenceableLoaderFactory,
|
compositeSequenceableLoaderFactory,
|
||||||
drmSessionManager != null ? drmSessionManager : mediaSourceDrmHelper.create(mediaItem),
|
drmSessionManager != null ? drmSessionManager : mediaSourceDrmHelper.create(mediaItem),
|
||||||
loadErrorHandlingPolicy,
|
loadErrorHandlingPolicy,
|
||||||
livePresentationDelayMs,
|
fallbackTargetLiveOffsetMs);
|
||||||
livePresentationDelayOverridesManifest);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @deprecated Use {@link #createMediaSource(MediaItem)} instead. */
|
/** @deprecated Use {@link #createMediaSource(MediaItem)} instead. */
|
||||||
@ -365,12 +369,21 @@ public final class DashMediaSource extends BaseMediaSource {
|
|||||||
boolean needsTag = mediaItem.playbackProperties.tag == null && tag != null;
|
boolean needsTag = mediaItem.playbackProperties.tag == null && tag != null;
|
||||||
boolean needsStreamKeys =
|
boolean needsStreamKeys =
|
||||||
mediaItem.playbackProperties.streamKeys.isEmpty() && !streamKeys.isEmpty();
|
mediaItem.playbackProperties.streamKeys.isEmpty() && !streamKeys.isEmpty();
|
||||||
if (needsTag && needsStreamKeys) {
|
boolean needsTargetLiveOffset =
|
||||||
mediaItem = mediaItem.buildUpon().setTag(tag).setStreamKeys(streamKeys).build();
|
mediaItem.liveConfiguration.targetLiveOffsetMs == C.TIME_UNSET
|
||||||
} else if (needsTag) {
|
&& targetLiveOffsetOverrideMs != C.TIME_UNSET;
|
||||||
mediaItem = mediaItem.buildUpon().setTag(tag).build();
|
if (needsTag || needsStreamKeys || needsTargetLiveOffset) {
|
||||||
} else if (needsStreamKeys) {
|
MediaItem.Builder builder = mediaItem.buildUpon();
|
||||||
mediaItem = mediaItem.buildUpon().setStreamKeys(streamKeys).build();
|
if (needsTag) {
|
||||||
|
builder.setTag(tag);
|
||||||
|
}
|
||||||
|
if (needsStreamKeys) {
|
||||||
|
builder.setStreamKeys(streamKeys);
|
||||||
|
}
|
||||||
|
if (needsTargetLiveOffset) {
|
||||||
|
builder.setLiveTargetOffsetMs(targetLiveOffsetOverrideMs);
|
||||||
|
}
|
||||||
|
mediaItem = builder.build();
|
||||||
}
|
}
|
||||||
return new DashMediaSource(
|
return new DashMediaSource(
|
||||||
mediaItem,
|
mediaItem,
|
||||||
@ -381,8 +394,7 @@ public final class DashMediaSource extends BaseMediaSource {
|
|||||||
compositeSequenceableLoaderFactory,
|
compositeSequenceableLoaderFactory,
|
||||||
drmSessionManager != null ? drmSessionManager : mediaSourceDrmHelper.create(mediaItem),
|
drmSessionManager != null ? drmSessionManager : mediaSourceDrmHelper.create(mediaItem),
|
||||||
loadErrorHandlingPolicy,
|
loadErrorHandlingPolicy,
|
||||||
livePresentationDelayMs,
|
fallbackTargetLiveOffsetMs);
|
||||||
livePresentationDelayOverridesManifest);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -392,17 +404,12 @@ public final class DashMediaSource extends BaseMediaSource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The default presentation delay for live streams. The presentation delay is the duration by
|
* The default target {@link Player#getCurrentLiveOffset() offset for live streams} that is used
|
||||||
* which the default start position precedes the end of the live window.
|
* if no value is defined in the {@link MediaItem} or the manifest.
|
||||||
*/
|
*/
|
||||||
public static final long DEFAULT_LIVE_PRESENTATION_DELAY_MS = 30_000;
|
public static final long DEFAULT_FALLBACK_TARGET_LIVE_OFFSET_MS = 30_000;
|
||||||
/** @deprecated Use {@link #DEFAULT_LIVE_PRESENTATION_DELAY_MS}. */
|
/** @deprecated Use {@link #DEFAULT_FALLBACK_TARGET_LIVE_OFFSET_MS} instead. */
|
||||||
@Deprecated
|
@Deprecated public static final long DEFAULT_LIVE_PRESENTATION_DELAY_MS = 30_000;
|
||||||
public static final long DEFAULT_LIVE_PRESENTATION_DELAY_FIXED_MS =
|
|
||||||
DEFAULT_LIVE_PRESENTATION_DELAY_MS;
|
|
||||||
/** @deprecated Use of this parameter is no longer necessary. */
|
|
||||||
@Deprecated public static final long DEFAULT_LIVE_PRESENTATION_DELAY_PREFER_MANIFEST_MS = -1;
|
|
||||||
|
|
||||||
/** The media id used by media items of dash media sources without a manifest URI. */
|
/** The media id used by media items of dash media sources without a manifest URI. */
|
||||||
public static final String DUMMY_MEDIA_ID =
|
public static final String DUMMY_MEDIA_ID =
|
||||||
"com.google.android.exoplayer2.source.dash.DashMediaSource";
|
"com.google.android.exoplayer2.source.dash.DashMediaSource";
|
||||||
@ -426,8 +433,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 long livePresentationDelayMs;
|
private final long fallbackTargetLiveOffsetMs;
|
||||||
private final boolean livePresentationDelayOverridesManifest;
|
|
||||||
private final EventDispatcher manifestEventDispatcher;
|
private final EventDispatcher manifestEventDispatcher;
|
||||||
private final ParsingLoadable.Parser<? extends DashManifest> manifestParser;
|
private final ParsingLoadable.Parser<? extends DashManifest> manifestParser;
|
||||||
private final ManifestCallback manifestCallback;
|
private final ManifestCallback manifestCallback;
|
||||||
@ -437,8 +443,6 @@ public final class DashMediaSource extends BaseMediaSource {
|
|||||||
private final Runnable simulateManifestRefreshRunnable;
|
private final Runnable simulateManifestRefreshRunnable;
|
||||||
private final PlayerEmsgCallback playerEmsgCallback;
|
private final PlayerEmsgCallback playerEmsgCallback;
|
||||||
private final LoaderErrorThrower manifestLoadErrorThrower;
|
private final LoaderErrorThrower manifestLoadErrorThrower;
|
||||||
private final MediaItem mediaItem;
|
|
||||||
private final MediaItem.PlaybackProperties playbackProperties;
|
|
||||||
|
|
||||||
private DataSource dataSource;
|
private DataSource dataSource;
|
||||||
private Loader loader;
|
private Loader loader;
|
||||||
@ -447,6 +451,8 @@ public final class DashMediaSource extends BaseMediaSource {
|
|||||||
private IOException manifestFatalError;
|
private IOException manifestFatalError;
|
||||||
private Handler handler;
|
private Handler handler;
|
||||||
|
|
||||||
|
private MediaItem mediaItem;
|
||||||
|
private MediaItem.PlaybackProperties playbackProperties;
|
||||||
private Uri manifestUri;
|
private Uri manifestUri;
|
||||||
private Uri initialManifestUri;
|
private Uri initialManifestUri;
|
||||||
private DashManifest manifest;
|
private DashManifest manifest;
|
||||||
@ -469,8 +475,7 @@ public final class DashMediaSource extends BaseMediaSource {
|
|||||||
CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory,
|
CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory,
|
||||||
DrmSessionManager drmSessionManager,
|
DrmSessionManager drmSessionManager,
|
||||||
LoadErrorHandlingPolicy loadErrorHandlingPolicy,
|
LoadErrorHandlingPolicy loadErrorHandlingPolicy,
|
||||||
long livePresentationDelayMs,
|
long fallbackTargetLiveOffsetMs) {
|
||||||
boolean livePresentationDelayOverridesManifest) {
|
|
||||||
this.mediaItem = mediaItem;
|
this.mediaItem = mediaItem;
|
||||||
this.playbackProperties = checkNotNull(mediaItem.playbackProperties);
|
this.playbackProperties = checkNotNull(mediaItem.playbackProperties);
|
||||||
this.manifestUri = playbackProperties.uri;
|
this.manifestUri = playbackProperties.uri;
|
||||||
@ -481,8 +486,7 @@ public final class DashMediaSource extends BaseMediaSource {
|
|||||||
this.chunkSourceFactory = chunkSourceFactory;
|
this.chunkSourceFactory = chunkSourceFactory;
|
||||||
this.drmSessionManager = drmSessionManager;
|
this.drmSessionManager = drmSessionManager;
|
||||||
this.loadErrorHandlingPolicy = loadErrorHandlingPolicy;
|
this.loadErrorHandlingPolicy = loadErrorHandlingPolicy;
|
||||||
this.livePresentationDelayMs = livePresentationDelayMs;
|
this.fallbackTargetLiveOffsetMs = fallbackTargetLiveOffsetMs;
|
||||||
this.livePresentationDelayOverridesManifest = livePresentationDelayOverridesManifest;
|
|
||||||
this.compositeSequenceableLoaderFactory = compositeSequenceableLoaderFactory;
|
this.compositeSequenceableLoaderFactory = compositeSequenceableLoaderFactory;
|
||||||
sideloadedManifest = manifest != null;
|
sideloadedManifest = manifest != null;
|
||||||
manifestEventDispatcher = createEventDispatcher(/* mediaPeriodId= */ null);
|
manifestEventDispatcher = createEventDispatcher(/* mediaPeriodId= */ null);
|
||||||
@ -688,6 +692,9 @@ public final class DashMediaSource extends BaseMediaSource {
|
|||||||
staleManifestReloadAttempt = 0;
|
staleManifestReloadAttempt = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mediaItem = mergeLiveConfiguration(mediaItem, fallbackTargetLiveOffsetMs, newManifest);
|
||||||
|
playbackProperties = castNonNull(mediaItem.playbackProperties);
|
||||||
|
|
||||||
manifest = newManifest;
|
manifest = newManifest;
|
||||||
manifestLoadPending &= manifest.dynamic;
|
manifestLoadPending &= manifest.dynamic;
|
||||||
manifestLoadStartTimestampMs = elapsedRealtimeMs - loadDurationMs;
|
manifestLoadStartTimestampMs = elapsedRealtimeMs - loadDurationMs;
|
||||||
@ -921,23 +928,7 @@ public final class DashMediaSource extends BaseMediaSource {
|
|||||||
for (int i = 0; i < manifest.getPeriodCount() - 1; i++) {
|
for (int i = 0; i < manifest.getPeriodCount() - 1; i++) {
|
||||||
windowDurationUs += manifest.getPeriodDurationUs(i);
|
windowDurationUs += manifest.getPeriodDurationUs(i);
|
||||||
}
|
}
|
||||||
long windowDefaultStartPositionUs = 0;
|
|
||||||
if (manifest.dynamic) {
|
|
||||||
long presentationDelayForManifestMs = livePresentationDelayMs;
|
|
||||||
if (!livePresentationDelayOverridesManifest
|
|
||||||
&& manifest.suggestedPresentationDelayMs != C.TIME_UNSET) {
|
|
||||||
presentationDelayForManifestMs = manifest.suggestedPresentationDelayMs;
|
|
||||||
}
|
|
||||||
// Snap the default position to the start of the segment containing it.
|
|
||||||
windowDefaultStartPositionUs = windowDurationUs - C.msToUs(presentationDelayForManifestMs);
|
|
||||||
if (windowDefaultStartPositionUs < MIN_LIVE_DEFAULT_START_POSITION_US) {
|
|
||||||
// The default start position is too close to the start of the live window. Set it to the
|
|
||||||
// minimum default start position provided the window is at least twice as big. Else set
|
|
||||||
// it to the middle of the window.
|
|
||||||
windowDefaultStartPositionUs =
|
|
||||||
min(MIN_LIVE_DEFAULT_START_POSITION_US, windowDurationUs / 2);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
long windowStartTimeMs = C.TIME_UNSET;
|
long windowStartTimeMs = C.TIME_UNSET;
|
||||||
if (manifest.availabilityStartTimeMs != C.TIME_UNSET) {
|
if (manifest.availabilityStartTimeMs != C.TIME_UNSET) {
|
||||||
windowStartTimeMs =
|
windowStartTimeMs =
|
||||||
@ -945,6 +936,25 @@ public final class DashMediaSource extends BaseMediaSource {
|
|||||||
+ manifest.getPeriod(0).startMs
|
+ manifest.getPeriod(0).startMs
|
||||||
+ C.usToMs(currentStartTimeUs);
|
+ C.usToMs(currentStartTimeUs);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
long windowDefaultStartPositionUs = 0;
|
||||||
|
if (manifest.dynamic) {
|
||||||
|
ensureTargetLiveOffsetIsInLiveWindow(
|
||||||
|
/* nowPeriodTimeUs= */ currentStartTimeUs + nowUnixTimeUs - C.msToUs(windowStartTimeMs),
|
||||||
|
/* windowStartPeriodTimeUs= */ currentStartTimeUs,
|
||||||
|
/* windowEndPeriodTimeUs= */ currentEndTimeUs);
|
||||||
|
windowDefaultStartPositionUs =
|
||||||
|
nowUnixTimeUs
|
||||||
|
- C.msToUs(windowStartTimeMs + mediaItem.liveConfiguration.targetLiveOffsetMs);
|
||||||
|
long minimumDefaultStartPositionUs =
|
||||||
|
min(MIN_LIVE_DEFAULT_START_POSITION_US, windowDurationUs / 2);
|
||||||
|
if (windowDefaultStartPositionUs < minimumDefaultStartPositionUs) {
|
||||||
|
// The default start position is too close to the start of the live window. Set it to the
|
||||||
|
// minimum default start position provided the window is at least twice as big. Else set
|
||||||
|
// it to the middle of the window.
|
||||||
|
windowDefaultStartPositionUs = minimumDefaultStartPositionUs;
|
||||||
|
}
|
||||||
|
}
|
||||||
DashTimeline timeline =
|
DashTimeline timeline =
|
||||||
new DashTimeline(
|
new DashTimeline(
|
||||||
manifest.availabilityStartTimeMs,
|
manifest.availabilityStartTimeMs,
|
||||||
@ -989,6 +999,26 @@ public final class DashMediaSource extends BaseMediaSource {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void ensureTargetLiveOffsetIsInLiveWindow(
|
||||||
|
long nowPeriodTimeUs, long windowStartPeriodTimeUs, long windowEndPeriodTimeUs) {
|
||||||
|
long targetLiveOffsetUs = C.msToUs(mediaItem.liveConfiguration.targetLiveOffsetMs);
|
||||||
|
long minOffsetUs = nowPeriodTimeUs - windowEndPeriodTimeUs;
|
||||||
|
if (targetLiveOffsetUs < minOffsetUs) {
|
||||||
|
targetLiveOffsetUs = minOffsetUs;
|
||||||
|
}
|
||||||
|
long maxOffsetUs = nowPeriodTimeUs - windowStartPeriodTimeUs;
|
||||||
|
if (targetLiveOffsetUs > maxOffsetUs) {
|
||||||
|
long windowDurationUs = windowEndPeriodTimeUs - windowStartPeriodTimeUs;
|
||||||
|
targetLiveOffsetUs =
|
||||||
|
maxOffsetUs - min(MIN_LIVE_DEFAULT_START_POSITION_US, windowDurationUs / 2);
|
||||||
|
}
|
||||||
|
long targetLiveOffsetMs = C.usToMs(targetLiveOffsetUs);
|
||||||
|
if (mediaItem.liveConfiguration.targetLiveOffsetMs != targetLiveOffsetMs) {
|
||||||
|
mediaItem = mediaItem.buildUpon().setLiveTargetOffsetMs(targetLiveOffsetMs).build();
|
||||||
|
playbackProperties = castNonNull(mediaItem.playbackProperties);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void scheduleManifestRefresh(long delayUntilNextLoadMs) {
|
private void scheduleManifestRefresh(long delayUntilNextLoadMs) {
|
||||||
handler.postDelayed(refreshManifestRunnable, delayUntilNextLoadMs);
|
handler.postDelayed(refreshManifestRunnable, delayUntilNextLoadMs);
|
||||||
}
|
}
|
||||||
@ -1057,6 +1087,41 @@ public final class DashMediaSource extends BaseMediaSource {
|
|||||||
return LongMath.divide(intervalUs, 1000, RoundingMode.CEILING);
|
return LongMath.divide(intervalUs, 1000, RoundingMode.CEILING);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static MediaItem mergeLiveConfiguration(
|
||||||
|
MediaItem mediaItem, long fallbackTargetLiveOffsetMs, DashManifest manifest) {
|
||||||
|
// Evaluate live config properties from media item and manifest according to precedence.
|
||||||
|
long liveTargetOffsetMs;
|
||||||
|
if (mediaItem.liveConfiguration.targetLiveOffsetMs != C.TIME_UNSET) {
|
||||||
|
liveTargetOffsetMs = mediaItem.liveConfiguration.targetLiveOffsetMs;
|
||||||
|
} else if (manifest.serviceDescription != null
|
||||||
|
&& manifest.serviceDescription.targetOffsetMs != C.TIME_UNSET) {
|
||||||
|
liveTargetOffsetMs = manifest.serviceDescription.targetOffsetMs;
|
||||||
|
} else if (manifest.suggestedPresentationDelayMs != C.TIME_UNSET) {
|
||||||
|
liveTargetOffsetMs = manifest.suggestedPresentationDelayMs;
|
||||||
|
} else {
|
||||||
|
liveTargetOffsetMs = fallbackTargetLiveOffsetMs;
|
||||||
|
}
|
||||||
|
float liveMinPlaybackSpeed = C.RATE_UNSET;
|
||||||
|
if (mediaItem.liveConfiguration.minPlaybackSpeed != C.RATE_UNSET) {
|
||||||
|
liveMinPlaybackSpeed = mediaItem.liveConfiguration.minPlaybackSpeed;
|
||||||
|
} else if (manifest.serviceDescription != null) {
|
||||||
|
liveMinPlaybackSpeed = manifest.serviceDescription.minPlaybackSpeed;
|
||||||
|
}
|
||||||
|
float liveMaxPlaybackSpeed = C.RATE_UNSET;
|
||||||
|
if (mediaItem.liveConfiguration.maxPlaybackSpeed != C.RATE_UNSET) {
|
||||||
|
liveMaxPlaybackSpeed = mediaItem.liveConfiguration.maxPlaybackSpeed;
|
||||||
|
} else if (manifest.serviceDescription != null) {
|
||||||
|
liveMaxPlaybackSpeed = manifest.serviceDescription.maxPlaybackSpeed;
|
||||||
|
}
|
||||||
|
// Update live configuration in the media item.
|
||||||
|
return mediaItem
|
||||||
|
.buildUpon()
|
||||||
|
.setLiveTargetOffsetMs(liveTargetOffsetMs)
|
||||||
|
.setLiveMinPlaybackSpeed(liveMinPlaybackSpeed)
|
||||||
|
.setLiveMaxPlaybackSpeed(liveMaxPlaybackSpeed)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
private static final class PeriodSeekInfo {
|
private static final class PeriodSeekInfo {
|
||||||
|
|
||||||
public static PeriodSeekInfo createPeriodSeekInfo(
|
public static PeriodSeekInfo createPeriodSeekInfo(
|
||||||
@ -1244,8 +1309,9 @@ public final class DashMediaSource extends BaseMediaSource {
|
|||||||
}
|
}
|
||||||
// If there are multiple video adaptation sets with unaligned segments, the initial time may
|
// If there are multiple video adaptation sets with unaligned segments, the initial time may
|
||||||
// not correspond to the start of a segment in both, but this is an edge case.
|
// not correspond to the start of a segment in both, but this is an edge case.
|
||||||
DashSegmentIndex snapIndex = period.adaptationSets.get(videoAdaptationSetIndex)
|
@Nullable
|
||||||
.representations.get(0).getIndex();
|
DashSegmentIndex snapIndex =
|
||||||
|
period.adaptationSets.get(videoAdaptationSetIndex).representations.get(0).getIndex();
|
||||||
if (snapIndex == null || snapIndex.getSegmentCount(periodDurationUs) == 0) {
|
if (snapIndex == null || snapIndex.getSegmentCount(periodDurationUs) == 0) {
|
||||||
// Video adaptation set does not include a non-empty index for snapping.
|
// Video adaptation set does not include a non-empty index for snapping.
|
||||||
return windowDefaultStartPositionUs;
|
return windowDefaultStartPositionUs;
|
||||||
|
@ -16,27 +16,56 @@
|
|||||||
package com.google.android.exoplayer2.source.dash;
|
package com.google.android.exoplayer2.source.dash;
|
||||||
|
|
||||||
import static com.google.common.truth.Truth.assertThat;
|
import static com.google.common.truth.Truth.assertThat;
|
||||||
|
import static java.util.concurrent.TimeUnit.MILLISECONDS;
|
||||||
import static org.junit.Assert.fail;
|
import static org.junit.Assert.fail;
|
||||||
|
import static org.robolectric.annotation.LooperMode.Mode.PAUSED;
|
||||||
|
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.test.core.app.ApplicationProvider;
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||||
|
import com.google.android.exoplayer2.C;
|
||||||
import com.google.android.exoplayer2.MediaItem;
|
import com.google.android.exoplayer2.MediaItem;
|
||||||
import com.google.android.exoplayer2.ParserException;
|
import com.google.android.exoplayer2.ParserException;
|
||||||
|
import com.google.android.exoplayer2.Timeline;
|
||||||
|
import com.google.android.exoplayer2.Timeline.Window;
|
||||||
import com.google.android.exoplayer2.offline.StreamKey;
|
import com.google.android.exoplayer2.offline.StreamKey;
|
||||||
|
import com.google.android.exoplayer2.source.MediaSource;
|
||||||
|
import com.google.android.exoplayer2.source.MediaSource.MediaSourceCaller;
|
||||||
|
import com.google.android.exoplayer2.testutil.TestUtil;
|
||||||
|
import com.google.android.exoplayer2.upstream.ByteArrayDataSource;
|
||||||
|
import com.google.android.exoplayer2.upstream.DataSource;
|
||||||
import com.google.android.exoplayer2.upstream.FileDataSource;
|
import com.google.android.exoplayer2.upstream.FileDataSource;
|
||||||
import com.google.android.exoplayer2.upstream.ParsingLoadable;
|
import com.google.android.exoplayer2.upstream.ParsingLoadable;
|
||||||
import com.google.android.exoplayer2.util.Util;
|
import com.google.android.exoplayer2.util.Util;
|
||||||
import com.google.common.collect.ImmutableList;
|
import com.google.common.collect.ImmutableList;
|
||||||
import java.io.ByteArrayInputStream;
|
import java.io.ByteArrayInputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.util.concurrent.CountDownLatch;
|
||||||
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.junit.runner.RunWith;
|
import org.junit.runner.RunWith;
|
||||||
|
import org.robolectric.annotation.LooperMode;
|
||||||
|
import org.robolectric.shadows.ShadowLooper;
|
||||||
|
|
||||||
/** Unit test for {@link DashMediaSource}. */
|
/** Unit test for {@link DashMediaSource}. */
|
||||||
@RunWith(AndroidJUnit4.class)
|
@RunWith(AndroidJUnit4.class)
|
||||||
|
@LooperMode(PAUSED)
|
||||||
public final class DashMediaSourceTest {
|
public final class DashMediaSourceTest {
|
||||||
|
|
||||||
|
private static final String SAMPLE_MPD_LIVE_WITHOUT_LIVE_CONFIGURATION =
|
||||||
|
"media/mpd/sample_mpd_live_without_live_configuration";
|
||||||
|
private static final String SAMPLE_MPD_LIVE_WITH_SUGGESTED_PRESENTATION_DELAY_2S =
|
||||||
|
"media/mpd/sample_mpd_live_with_suggested_presentation_delay_2s";
|
||||||
|
private static final String SAMPLE_MPD_LIVE_WITH_COMPLETE_SERVICE_DESCRIPTION =
|
||||||
|
"media/mpd/sample_mpd_live_with_complete_service_description";
|
||||||
|
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_LIVE_WITH_OFFSET_TOO_SHORT =
|
||||||
|
"media/mpd/sample_mpd_live_with_offset_too_short";
|
||||||
|
private static final String SAMPLE_MPD_LIVE_WITH_OFFSET_TOO_LONG =
|
||||||
|
"media/mpd/sample_mpd_live_with_offset_too_long";
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void iso8601ParserParse() throws IOException {
|
public void iso8601ParserParse() throws IOException {
|
||||||
DashMediaSource.Iso8601Parser parser = new DashMediaSource.Iso8601Parser();
|
DashMediaSource.Iso8601Parser parser = new DashMediaSource.Iso8601Parser();
|
||||||
@ -157,7 +186,7 @@ public final class DashMediaSourceTest {
|
|||||||
// Tests backwards compatibility
|
// Tests backwards compatibility
|
||||||
@SuppressWarnings("deprecation")
|
@SuppressWarnings("deprecation")
|
||||||
@Test
|
@Test
|
||||||
public void factorySetStreamKeys_withMediaItemStreamKeys_doesNotsOverrideMediaItemStreamKeys() {
|
public void factorySetStreamKeys_withMediaItemStreamKeys_doesNotOverrideMediaItemStreamKeys() {
|
||||||
StreamKey mediaItemStreamKey = new StreamKey(/* groupIndex= */ 0, /* trackIndex= */ 1);
|
StreamKey mediaItemStreamKey = new StreamKey(/* groupIndex= */ 0, /* trackIndex= */ 1);
|
||||||
MediaItem mediaItem =
|
MediaItem mediaItem =
|
||||||
new MediaItem.Builder()
|
new MediaItem.Builder()
|
||||||
@ -187,6 +216,280 @@ public final class DashMediaSourceTest {
|
|||||||
assertThat(mediaSource.getMediaItem()).isEqualTo(mediaItem);
|
assertThat(mediaSource.getMediaItem()).isEqualTo(mediaItem);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void factorySetFallbackTargetLiveOffsetMs_withMediaLiveTargetOffsetMs_usesMediaOffset() {
|
||||||
|
MediaItem mediaItem =
|
||||||
|
new MediaItem.Builder().setUri(Uri.EMPTY).setLiveTargetOffsetMs(2L).build();
|
||||||
|
DashMediaSource.Factory factory =
|
||||||
|
new DashMediaSource.Factory(new FileDataSource.Factory())
|
||||||
|
.setFallbackTargetLiveOffsetMs(1234L);
|
||||||
|
|
||||||
|
MediaItem dashMediaItem = factory.createMediaSource(mediaItem).getMediaItem();
|
||||||
|
|
||||||
|
assertThat(dashMediaItem.liveConfiguration.targetLiveOffsetMs).isEqualTo(2L);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void factorySetLivePresentationDelayMs_withMediaLiveTargetOffset_usesMediaOffset() {
|
||||||
|
MediaItem mediaItem =
|
||||||
|
new MediaItem.Builder().setUri(Uri.EMPTY).setLiveTargetOffsetMs(2L).build();
|
||||||
|
DashMediaSource.Factory factory =
|
||||||
|
new DashMediaSource.Factory(new FileDataSource.Factory())
|
||||||
|
.setLivePresentationDelayMs(1234L, /* overridesManifest= */ true);
|
||||||
|
|
||||||
|
MediaItem dashMediaItem = factory.createMediaSource(mediaItem).getMediaItem();
|
||||||
|
|
||||||
|
assertThat(dashMediaItem.liveConfiguration.targetLiveOffsetMs).isEqualTo(2L);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void factorySetLivePresentationDelayMs_overridingManifest_mixedIntoMediaItem() {
|
||||||
|
MediaItem mediaItem = new MediaItem.Builder().setUri(Uri.EMPTY).build();
|
||||||
|
DashMediaSource.Factory factory =
|
||||||
|
new DashMediaSource.Factory(new FileDataSource.Factory())
|
||||||
|
.setLivePresentationDelayMs(2000L, /* overridesManifest= */ true);
|
||||||
|
|
||||||
|
MediaItem dashMediaItem = factory.createMediaSource(mediaItem).getMediaItem();
|
||||||
|
|
||||||
|
assertThat(dashMediaItem.liveConfiguration.targetLiveOffsetMs).isEqualTo(2000L);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void factorySetLivePresentationDelayMs_notOverridingManifest_unsetInMediaItem() {
|
||||||
|
MediaItem mediaItem = new MediaItem.Builder().setUri(Uri.EMPTY).build();
|
||||||
|
DashMediaSource.Factory factory =
|
||||||
|
new DashMediaSource.Factory(new FileDataSource.Factory())
|
||||||
|
.setLivePresentationDelayMs(2000L, /* overridesManifest= */ false);
|
||||||
|
|
||||||
|
MediaItem dashMediaItem = factory.createMediaSource(mediaItem).getMediaItem();
|
||||||
|
|
||||||
|
assertThat(dashMediaItem.liveConfiguration.targetLiveOffsetMs).isEqualTo(C.TIME_UNSET);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void factorySetFallbackTargetLiveOffsetMs_doesNotChangeMediaItem() {
|
||||||
|
DashMediaSource.Factory factory =
|
||||||
|
new DashMediaSource.Factory(new FileDataSource.Factory())
|
||||||
|
.setFallbackTargetLiveOffsetMs(2000L);
|
||||||
|
|
||||||
|
MediaItem dashMediaItem =
|
||||||
|
factory.createMediaSource(MediaItem.fromUri(Uri.EMPTY)).getMediaItem();
|
||||||
|
|
||||||
|
assertThat(dashMediaItem.liveConfiguration.targetLiveOffsetMs).isEqualTo(C.TIME_UNSET);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void prepare_withoutLiveConfiguration_withoutMediaItemLiveProperties_usesDefaultFallback()
|
||||||
|
throws InterruptedException {
|
||||||
|
DashMediaSource mediaSource =
|
||||||
|
new DashMediaSource.Factory(
|
||||||
|
() -> createSampleMpdDataSource(SAMPLE_MPD_LIVE_WITHOUT_LIVE_CONFIGURATION))
|
||||||
|
.createMediaSource(MediaItem.fromUri(Uri.EMPTY));
|
||||||
|
|
||||||
|
MediaItem mediaItemFromSource = prepareAndWaitForTimelineRefresh(mediaSource).mediaItem;
|
||||||
|
|
||||||
|
assertThat(mediaItemFromSource.liveConfiguration.targetLiveOffsetMs)
|
||||||
|
.isEqualTo(DashMediaSource.DEFAULT_FALLBACK_TARGET_LIVE_OFFSET_MS);
|
||||||
|
assertThat(mediaItemFromSource.liveConfiguration.minPlaybackSpeed).isEqualTo(C.RATE_UNSET);
|
||||||
|
assertThat(mediaItemFromSource.liveConfiguration.maxPlaybackSpeed).isEqualTo(C.RATE_UNSET);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void prepare_withoutLiveConfiguration_withoutMediaItemLiveProperties_usesFallback()
|
||||||
|
throws InterruptedException {
|
||||||
|
DashMediaSource mediaSource =
|
||||||
|
new DashMediaSource.Factory(
|
||||||
|
() -> createSampleMpdDataSource(SAMPLE_MPD_LIVE_WITHOUT_LIVE_CONFIGURATION))
|
||||||
|
.setFallbackTargetLiveOffsetMs(1234L)
|
||||||
|
.createMediaSource(MediaItem.fromUri(Uri.EMPTY));
|
||||||
|
|
||||||
|
MediaItem mediaItemFromSource = prepareAndWaitForTimelineRefresh(mediaSource).mediaItem;
|
||||||
|
|
||||||
|
assertThat(mediaItemFromSource.liveConfiguration.targetLiveOffsetMs).isEqualTo(1234L);
|
||||||
|
assertThat(mediaItemFromSource.liveConfiguration.minPlaybackSpeed).isEqualTo(C.RATE_UNSET);
|
||||||
|
assertThat(mediaItemFromSource.liveConfiguration.maxPlaybackSpeed).isEqualTo(C.RATE_UNSET);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void prepare_withoutLiveConfiguration_withMediaItemLiveProperties_usesMediaItem()
|
||||||
|
throws InterruptedException {
|
||||||
|
MediaItem mediaItem =
|
||||||
|
new MediaItem.Builder()
|
||||||
|
.setUri(Uri.EMPTY)
|
||||||
|
.setLiveTargetOffsetMs(876L)
|
||||||
|
.setLiveMinPlaybackSpeed(23f)
|
||||||
|
.setLiveMaxPlaybackSpeed(42f)
|
||||||
|
.build();
|
||||||
|
DashMediaSource mediaSource =
|
||||||
|
new DashMediaSource.Factory(
|
||||||
|
() -> createSampleMpdDataSource(SAMPLE_MPD_LIVE_WITHOUT_LIVE_CONFIGURATION))
|
||||||
|
.setFallbackTargetLiveOffsetMs(1234L)
|
||||||
|
.createMediaSource(mediaItem);
|
||||||
|
|
||||||
|
MediaItem mediaItemFromSource = prepareAndWaitForTimelineRefresh(mediaSource).mediaItem;
|
||||||
|
|
||||||
|
assertThat(mediaItemFromSource.liveConfiguration.targetLiveOffsetMs).isEqualTo(876L);
|
||||||
|
assertThat(mediaItemFromSource.liveConfiguration.minPlaybackSpeed).isEqualTo(23f);
|
||||||
|
assertThat(mediaItemFromSource.liveConfiguration.maxPlaybackSpeed).isEqualTo(42f);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void prepare_withSuggestedPresentationDelay_usesManifestValue()
|
||||||
|
throws InterruptedException {
|
||||||
|
DashMediaSource mediaSource =
|
||||||
|
new DashMediaSource.Factory(
|
||||||
|
() ->
|
||||||
|
createSampleMpdDataSource(SAMPLE_MPD_LIVE_WITH_SUGGESTED_PRESENTATION_DELAY_2S))
|
||||||
|
.setFallbackTargetLiveOffsetMs(1234L)
|
||||||
|
.createMediaSource(MediaItem.fromUri(Uri.EMPTY));
|
||||||
|
|
||||||
|
MediaItem mediaItem = prepareAndWaitForTimelineRefresh(mediaSource).mediaItem;
|
||||||
|
|
||||||
|
assertThat(mediaItem.liveConfiguration.targetLiveOffsetMs).isEqualTo(2_000L);
|
||||||
|
assertThat(mediaItem.liveConfiguration.minPlaybackSpeed).isEqualTo(C.RATE_UNSET);
|
||||||
|
assertThat(mediaItem.liveConfiguration.maxPlaybackSpeed).isEqualTo(C.RATE_UNSET);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void prepare_withSuggestedPresentationDelay_withMediaItemLiveProperties_usesMediaItem()
|
||||||
|
throws InterruptedException {
|
||||||
|
MediaItem mediaItem =
|
||||||
|
new MediaItem.Builder()
|
||||||
|
.setUri(Uri.EMPTY)
|
||||||
|
.setLiveTargetOffsetMs(876L)
|
||||||
|
.setLiveMinPlaybackSpeed(23f)
|
||||||
|
.setLiveMaxPlaybackSpeed(42f)
|
||||||
|
.build();
|
||||||
|
DashMediaSource mediaSource =
|
||||||
|
new DashMediaSource.Factory(
|
||||||
|
() ->
|
||||||
|
createSampleMpdDataSource(SAMPLE_MPD_LIVE_WITH_SUGGESTED_PRESENTATION_DELAY_2S))
|
||||||
|
.setFallbackTargetLiveOffsetMs(1234L)
|
||||||
|
.createMediaSource(mediaItem);
|
||||||
|
|
||||||
|
MediaItem mediaItemFromSource = prepareAndWaitForTimelineRefresh(mediaSource).mediaItem;
|
||||||
|
|
||||||
|
assertThat(mediaItemFromSource.liveConfiguration.targetLiveOffsetMs).isEqualTo(876L);
|
||||||
|
assertThat(mediaItemFromSource.liveConfiguration.minPlaybackSpeed).isEqualTo(23f);
|
||||||
|
assertThat(mediaItemFromSource.liveConfiguration.maxPlaybackSpeed).isEqualTo(42f);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void prepare_withCompleteServiceDescription_usesManifestValue()
|
||||||
|
throws InterruptedException {
|
||||||
|
DashMediaSource mediaSource =
|
||||||
|
new DashMediaSource.Factory(
|
||||||
|
() -> createSampleMpdDataSource(SAMPLE_MPD_LIVE_WITH_COMPLETE_SERVICE_DESCRIPTION))
|
||||||
|
.setFallbackTargetLiveOffsetMs(1234L)
|
||||||
|
.createMediaSource(MediaItem.fromUri(Uri.EMPTY));
|
||||||
|
|
||||||
|
MediaItem mediaItem = prepareAndWaitForTimelineRefresh(mediaSource).mediaItem;
|
||||||
|
|
||||||
|
assertThat(mediaItem.liveConfiguration.targetLiveOffsetMs).isEqualTo(4_000L);
|
||||||
|
assertThat(mediaItem.liveConfiguration.minPlaybackSpeed).isEqualTo(0.96f);
|
||||||
|
assertThat(mediaItem.liveConfiguration.maxPlaybackSpeed).isEqualTo(1.04f);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void prepare_withCompleteServiceDescription_withMediaItemLiveProperties_usesMediaItem()
|
||||||
|
throws InterruptedException {
|
||||||
|
MediaItem mediaItem =
|
||||||
|
new MediaItem.Builder()
|
||||||
|
.setUri(Uri.EMPTY)
|
||||||
|
.setLiveTargetOffsetMs(876L)
|
||||||
|
.setLiveMinPlaybackSpeed(23f)
|
||||||
|
.setLiveMaxPlaybackSpeed(42f)
|
||||||
|
.build();
|
||||||
|
DashMediaSource mediaSource =
|
||||||
|
new DashMediaSource.Factory(
|
||||||
|
() -> createSampleMpdDataSource(SAMPLE_MPD_LIVE_WITH_COMPLETE_SERVICE_DESCRIPTION))
|
||||||
|
.setFallbackTargetLiveOffsetMs(1234L)
|
||||||
|
.createMediaSource(mediaItem);
|
||||||
|
|
||||||
|
MediaItem mediaItemFromSource = prepareAndWaitForTimelineRefresh(mediaSource).mediaItem;
|
||||||
|
|
||||||
|
assertThat(mediaItemFromSource.liveConfiguration.targetLiveOffsetMs).isEqualTo(876L);
|
||||||
|
assertThat(mediaItemFromSource.liveConfiguration.minPlaybackSpeed).isEqualTo(23f);
|
||||||
|
assertThat(mediaItemFromSource.liveConfiguration.maxPlaybackSpeed).isEqualTo(42f);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void prepare_targetLiveOffsetInWindow_manifestTargetOffsetAndAlignedWindowStartPosition()
|
||||||
|
throws InterruptedException {
|
||||||
|
DashMediaSource mediaSource =
|
||||||
|
new DashMediaSource.Factory(
|
||||||
|
() -> createSampleMpdDataSource(SAMPLE_MPD_LIVE_WITH_OFFSET_INSIDE_WINDOW))
|
||||||
|
.createMediaSource(MediaItem.fromUri(Uri.EMPTY));
|
||||||
|
|
||||||
|
Window window = prepareAndWaitForTimelineRefresh(mediaSource);
|
||||||
|
|
||||||
|
// Expect the target live offset as defined in the manifest.
|
||||||
|
assertThat(window.mediaItem.liveConfiguration.targetLiveOffsetMs).isEqualTo(3000);
|
||||||
|
// Expect the default position at the first segment start before the live edge.
|
||||||
|
assertThat(window.getDefaultPositionMs()).isEqualTo(2_000);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void prepare_targetLiveOffsetTooLong_correctedTargetOffsetAndAlignedWindowStartPosition()
|
||||||
|
throws InterruptedException {
|
||||||
|
DashMediaSource mediaSource =
|
||||||
|
new DashMediaSource.Factory(
|
||||||
|
() -> createSampleMpdDataSource(SAMPLE_MPD_LIVE_WITH_OFFSET_TOO_LONG))
|
||||||
|
.createMediaSource(MediaItem.fromUri(Uri.EMPTY));
|
||||||
|
|
||||||
|
Window window = prepareAndWaitForTimelineRefresh(mediaSource);
|
||||||
|
|
||||||
|
// Expect the default position at the first segment start below the minimum live start position.
|
||||||
|
assertThat(window.getDefaultPositionMs()).isEqualTo(4_000);
|
||||||
|
// Expect the target live offset reaching from now time to the minimum live start position.
|
||||||
|
assertThat(window.mediaItem.liveConfiguration.targetLiveOffsetMs).isEqualTo(9000);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void prepare_targetLiveOffsetTooShort_correctedTargetOffsetAndAlignedWindowStartPosition()
|
||||||
|
throws InterruptedException {
|
||||||
|
// Load manifest with now time far behind the start of the window.
|
||||||
|
DashMediaSource mediaSource =
|
||||||
|
new DashMediaSource.Factory(
|
||||||
|
() -> createSampleMpdDataSource(SAMPLE_MPD_LIVE_WITH_OFFSET_TOO_SHORT))
|
||||||
|
.createMediaSource(MediaItem.fromUri(Uri.EMPTY));
|
||||||
|
|
||||||
|
Window window = prepareAndWaitForTimelineRefresh(mediaSource);
|
||||||
|
|
||||||
|
// Expect the default position at the start of the last segment.
|
||||||
|
assertThat(window.getDefaultPositionMs()).isEqualTo(12_000);
|
||||||
|
// Expect the target live offset reaching from now time to the end of the window.
|
||||||
|
assertThat(window.mediaItem.liveConfiguration.targetLiveOffsetMs).isEqualTo(60_000 - 16_000);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Window prepareAndWaitForTimelineRefresh(MediaSource mediaSource)
|
||||||
|
throws InterruptedException {
|
||||||
|
AtomicReference<Window> windowReference = new AtomicReference<>();
|
||||||
|
CountDownLatch countDownLatch = new CountDownLatch(/* count= */ 1);
|
||||||
|
MediaSourceCaller caller =
|
||||||
|
(MediaSource source, Timeline timeline) -> {
|
||||||
|
if (windowReference.get() == null) {
|
||||||
|
windowReference.set(timeline.getWindow(0, new Timeline.Window()));
|
||||||
|
countDownLatch.countDown();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
mediaSource.prepareSource(caller, /* mediaTransferListener= */ null);
|
||||||
|
while (!countDownLatch.await(/* timeout= */ 10, MILLISECONDS)) {
|
||||||
|
ShadowLooper.idleMainLooper();
|
||||||
|
}
|
||||||
|
return windowReference.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static DataSource createSampleMpdDataSource(String fileName) {
|
||||||
|
byte[] manifestData = new byte[0];
|
||||||
|
try {
|
||||||
|
manifestData = TestUtil.getByteArray(ApplicationProvider.getApplicationContext(), fileName);
|
||||||
|
} catch (IOException e) {
|
||||||
|
fail(e.getMessage());
|
||||||
|
}
|
||||||
|
return new ByteArrayDataSource(manifestData);
|
||||||
|
}
|
||||||
|
|
||||||
private static void assertParseStringToLong(
|
private static void assertParseStringToLong(
|
||||||
long expected, ParsingLoadable.Parser<Long> parser, String data) throws IOException {
|
long expected, ParsingLoadable.Parser<Long> parser, String data) throws IOException {
|
||||||
long actual = parser.parse(null, new ByteArrayInputStream(Util.getUtf8Bytes(data)));
|
long actual = parser.parse(null, new ByteArrayInputStream(Util.getUtf8Bytes(data)));
|
||||||
|
27
testdata/src/test/assets/media/mpd/sample_mpd_live_with_complete_service_description
vendored
Normal file
27
testdata/src/test/assets/media/mpd/sample_mpd_live_with_complete_service_description
vendored
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<MPD
|
||||||
|
type="dynamic"
|
||||||
|
suggestedPresentationDelay="PT2S"
|
||||||
|
availabilityStartTime="2020-01-01T00:00:00Z"
|
||||||
|
minimumUpdatePeriod="PT4M"
|
||||||
|
timeShiftBufferDepth="PT6.0S">
|
||||||
|
<UTCTiming
|
||||||
|
schemeIdUri="urn:mpeg:dash:utc:direct:2014"
|
||||||
|
value="2020-01-01T01:00:00Z" />
|
||||||
|
<ServiceDescription id="0">
|
||||||
|
<Latency target="4000" />
|
||||||
|
<PlaybackRate max="1.04" min="0.96" />
|
||||||
|
</ServiceDescription>
|
||||||
|
<Period start="PT0.0S">
|
||||||
|
<AdaptationSet contentType="video">
|
||||||
|
<Representation id="0" mimeType="video/mp4">
|
||||||
|
<SegmentTemplate
|
||||||
|
timescale="1000000"
|
||||||
|
duration="2000000"
|
||||||
|
availabilityTimeOffset="2"
|
||||||
|
startNumber="1">
|
||||||
|
</SegmentTemplate>
|
||||||
|
</Representation>
|
||||||
|
</AdaptationSet>
|
||||||
|
</Period>
|
||||||
|
</MPD>
|
24
testdata/src/test/assets/media/mpd/sample_mpd_live_with_offset_inside_window
vendored
Normal file
24
testdata/src/test/assets/media/mpd/sample_mpd_live_with_offset_inside_window
vendored
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<MPD
|
||||||
|
type="dynamic"
|
||||||
|
minimumUpdatePeriod="PT4M"
|
||||||
|
availabilityStartTime="2020-01-01T00:00:00Z"
|
||||||
|
timeShiftBufferDepth="PT6.0S">
|
||||||
|
<UTCTiming
|
||||||
|
schemeIdUri="urn:mpeg:dash:utc:direct:2014"
|
||||||
|
value="2020-01-01T01:00:00Z" />
|
||||||
|
<ServiceDescription id="0">
|
||||||
|
<Latency target="3000" />
|
||||||
|
</ServiceDescription>
|
||||||
|
<Period start="PT0.0S">
|
||||||
|
<AdaptationSet contentType="video">
|
||||||
|
<Representation id="0" mimeType="video/mp4">
|
||||||
|
<SegmentTemplate
|
||||||
|
timescale="1000000"
|
||||||
|
duration="2000000"
|
||||||
|
availabilityTimeOffset="2"
|
||||||
|
startNumber="1"/>
|
||||||
|
</Representation>
|
||||||
|
</AdaptationSet>
|
||||||
|
</Period>
|
||||||
|
</MPD>
|
24
testdata/src/test/assets/media/mpd/sample_mpd_live_with_offset_too_long
vendored
Normal file
24
testdata/src/test/assets/media/mpd/sample_mpd_live_with_offset_too_long
vendored
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<MPD
|
||||||
|
type="dynamic"
|
||||||
|
minimumUpdatePeriod="PT4M"
|
||||||
|
availabilityStartTime="2020-01-01T00:00:00Z"
|
||||||
|
timeShiftBufferDepth="PT16.0S">
|
||||||
|
<UTCTiming
|
||||||
|
schemeIdUri="urn:mpeg:dash:utc:direct:2014"
|
||||||
|
value="2020-01-01T00:00:20Z" />
|
||||||
|
<ServiceDescription id="0">
|
||||||
|
<Latency target="30000" />
|
||||||
|
</ServiceDescription>
|
||||||
|
<Period start="PT0.0S">
|
||||||
|
<AdaptationSet contentType="video">
|
||||||
|
<Representation id="0" mimeType="video/mp4">
|
||||||
|
<SegmentTemplate
|
||||||
|
timescale="1000000"
|
||||||
|
duration="2000000"
|
||||||
|
availabilityTimeOffset="2"
|
||||||
|
startNumber="1"/>
|
||||||
|
</Representation>
|
||||||
|
</AdaptationSet>
|
||||||
|
</Period>
|
||||||
|
</MPD>
|
25
testdata/src/test/assets/media/mpd/sample_mpd_live_with_offset_too_short
vendored
Normal file
25
testdata/src/test/assets/media/mpd/sample_mpd_live_with_offset_too_short
vendored
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<MPD
|
||||||
|
type="dynamic"
|
||||||
|
timeShiftBufferDepth="PT16S"
|
||||||
|
minimumUpdatePeriod="PT4M"
|
||||||
|
availabilityStartTime="1970-01-01T00:00:00Z">
|
||||||
|
<!-- Now is 60 seconds after the start of the window. -->
|
||||||
|
<UTCTiming
|
||||||
|
schemeIdUri="urn:mpeg:dash:utc:direct:2014"
|
||||||
|
value="2020-01-01T00:01:00Z" />
|
||||||
|
<ServiceDescription id="0">
|
||||||
|
<Latency target="4000" />
|
||||||
|
</ServiceDescription>
|
||||||
|
<Period id="1" start="PT0S">
|
||||||
|
<AdaptationSet id="0" contentType="video">
|
||||||
|
<SegmentTemplate presentationTimeOffset="0" timescale="1000" startNumber="1">
|
||||||
|
<SegmentTimeline>
|
||||||
|
<!-- t = 2020-01-01T00:00:00Z (UTC) -->
|
||||||
|
<S t="1577836800000" d="4000" r="3"/>
|
||||||
|
</SegmentTimeline>
|
||||||
|
</SegmentTemplate>
|
||||||
|
<Representation id="0"/>
|
||||||
|
</AdaptationSet>
|
||||||
|
</Period>
|
||||||
|
</MPD>
|
23
testdata/src/test/assets/media/mpd/sample_mpd_live_with_suggested_presentation_delay_2s
vendored
Normal file
23
testdata/src/test/assets/media/mpd/sample_mpd_live_with_suggested_presentation_delay_2s
vendored
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<MPD
|
||||||
|
type="dynamic"
|
||||||
|
suggestedPresentationDelay="PT2S"
|
||||||
|
minimumUpdatePeriod="PT4M"
|
||||||
|
availabilityStartTime="2020-01-01T00:00:00Z"
|
||||||
|
timeShiftBufferDepth="PT6.0S">
|
||||||
|
<UTCTiming
|
||||||
|
schemeIdUri="urn:mpeg:dash:utc:direct:2014"
|
||||||
|
value="2020-01-01T01:00:00Z" />
|
||||||
|
<Period start="PT0.0S">
|
||||||
|
<AdaptationSet contentType="video">
|
||||||
|
<Representation id="0" mimeType="video/mp4">
|
||||||
|
<SegmentTemplate
|
||||||
|
timescale="1000000"
|
||||||
|
duration="2000000"
|
||||||
|
availabilityTimeOffset="2"
|
||||||
|
startNumber="1">
|
||||||
|
</SegmentTemplate>
|
||||||
|
</Representation>
|
||||||
|
</AdaptationSet>
|
||||||
|
</Period>
|
||||||
|
</MPD>
|
22
testdata/src/test/assets/media/mpd/sample_mpd_live_without_live_configuration
vendored
Normal file
22
testdata/src/test/assets/media/mpd/sample_mpd_live_without_live_configuration
vendored
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<MPD
|
||||||
|
type="dynamic"
|
||||||
|
minimumUpdatePeriod="PT4M"
|
||||||
|
availabilityStartTime="2020-01-01T00:00:00Z"
|
||||||
|
timeShiftBufferDepth="PT1M">
|
||||||
|
<UTCTiming
|
||||||
|
schemeIdUri="urn:mpeg:dash:utc:direct:2014"
|
||||||
|
value="2020-01-01T01:00:00Z" />
|
||||||
|
<Period start="PT0.0S">
|
||||||
|
<AdaptationSet contentType="video">
|
||||||
|
<Representation id="0" mimeType="video/mp4">
|
||||||
|
<SegmentTemplate
|
||||||
|
timescale="1000000"
|
||||||
|
duration="2000000"
|
||||||
|
availabilityTimeOffset="2"
|
||||||
|
startNumber="1">
|
||||||
|
</SegmentTemplate>
|
||||||
|
</Representation>
|
||||||
|
</AdaptationSet>
|
||||||
|
</Period>
|
||||||
|
</MPD>
|
Loading…
x
Reference in New Issue
Block a user