Use low latency properties in DashMediaSource

Issue: #4904
PiperOrigin-RevId: 337046645
This commit is contained in:
christosts 2020-10-14 10:13:10 +01:00 committed by Oliver Woodman
parent 6f66e7d0ba
commit 41b58d503a
8 changed files with 585 additions and 71 deletions

View File

@ -16,6 +16,7 @@
package com.google.android.exoplayer2.source.dash;
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.min;
@ -29,6 +30,7 @@ import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ExoPlayerLibraryInfo;
import com.google.android.exoplayer2.MediaItem;
import com.google.android.exoplayer2.ParserException;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.drm.DrmSessionEventListener;
import com.google.android.exoplayer2.drm.DrmSessionManager;
@ -102,8 +104,8 @@ public final class DashMediaSource extends BaseMediaSource {
@Nullable private DrmSessionManager drmSessionManager;
private CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory;
private LoadErrorHandlingPolicy loadErrorHandlingPolicy;
private long livePresentationDelayMs;
private boolean livePresentationDelayOverridesManifest;
private long targetLiveOffsetOverrideMs;
private long fallbackTargetLiveOffsetMs;
@Nullable private ParsingLoadable.Parser<? extends DashManifest> manifestParser;
private List<StreamKey> streamKeys;
@Nullable private Object tag;
@ -134,7 +136,8 @@ public final class DashMediaSource extends BaseMediaSource {
this.manifestDataSourceFactory = manifestDataSourceFactory;
mediaSourceDrmHelper = new MediaSourceDrmHelper();
loadErrorHandlingPolicy = new DefaultLoadErrorHandlingPolicy();
livePresentationDelayMs = DEFAULT_LIVE_PRESENTATION_DELAY_MS;
targetLiveOffsetOverrideMs = C.TIME_UNSET;
fallbackTargetLiveOffsetMs = DEFAULT_FALLBACK_TARGET_LIVE_OFFSET_MS;
compositeSequenceableLoaderFactory = new DefaultCompositeSequenceableLoaderFactory();
streamKeys = Collections.emptyList();
}
@ -204,34 +207,31 @@ public final class DashMediaSource extends BaseMediaSource {
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
@SuppressWarnings("deprecation")
public Factory setLivePresentationDelayMs(long livePresentationDelayMs) {
if (livePresentationDelayMs == DEFAULT_LIVE_PRESENTATION_DELAY_PREFER_MANIFEST_MS) {
return setLivePresentationDelayMs(DEFAULT_LIVE_PRESENTATION_DELAY_MS, false);
} else {
return setLivePresentationDelayMs(livePresentationDelayMs, true);
public Factory setLivePresentationDelayMs(
long livePresentationDelayMs, boolean overridesManifest) {
targetLiveOffsetOverrideMs = overridesManifest ? livePresentationDelayMs : C.TIME_UNSET;
if (!overridesManifest) {
setFallbackTargetLiveOffsetMs(livePresentationDelayMs);
}
return this;
}
/**
* Sets the duration in milliseconds by which the default start position should precede the end
* of the live window for live playbacks. The {@code overridesManifest} parameter specifies
* 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.
* Sets the {@link Player#getCurrentLiveOffset() target offset for live streams} that is used if
* no value is defined in the {@link MediaItem} or the manifest.
*
* @param livePresentationDelayMs For live playbacks, the duration in milliseconds by which the
* 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
* present.
* <p>The default value is {@link #DEFAULT_FALLBACK_TARGET_LIVE_OFFSET_MS}.
*
* @param fallbackTargetLiveOffsetMs The fallback live target offset in milliseconds.
* @return This factory, for convenience.
*/
public Factory setLivePresentationDelayMs(
long livePresentationDelayMs, boolean overridesManifest) {
this.livePresentationDelayMs = livePresentationDelayMs;
this.livePresentationDelayOverridesManifest = overridesManifest;
public Factory setFallbackTargetLiveOffsetMs(long fallbackTargetLiveOffsetMs) {
this.fallbackTargetLiveOffsetMs = fallbackTargetLiveOffsetMs;
return this;
}
@ -306,12 +306,17 @@ public final class DashMediaSource extends BaseMediaSource {
}
boolean hasUri = mediaItem.playbackProperties != null;
boolean hasTag = hasUri && mediaItem.playbackProperties.tag != null;
boolean hasTargetLiveOffset = mediaItem.liveConfiguration.targetLiveOffsetMs != C.TIME_UNSET;
mediaItem =
mediaItem
.buildUpon()
.setMimeType(MimeTypes.APPLICATION_MPD)
.setUri(hasUri ? mediaItem.playbackProperties.uri : Uri.EMPTY)
.setTag(hasTag ? mediaItem.playbackProperties.tag : tag)
.setLiveTargetOffsetMs(
hasTargetLiveOffset
? mediaItem.liveConfiguration.targetLiveOffsetMs
: targetLiveOffsetOverrideMs)
.setStreamKeys(streamKeys)
.build();
return new DashMediaSource(
@ -323,8 +328,7 @@ public final class DashMediaSource extends BaseMediaSource {
compositeSequenceableLoaderFactory,
drmSessionManager != null ? drmSessionManager : mediaSourceDrmHelper.create(mediaItem),
loadErrorHandlingPolicy,
livePresentationDelayMs,
livePresentationDelayOverridesManifest);
fallbackTargetLiveOffsetMs);
}
/** @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 needsStreamKeys =
mediaItem.playbackProperties.streamKeys.isEmpty() && !streamKeys.isEmpty();
if (needsTag && needsStreamKeys) {
mediaItem = mediaItem.buildUpon().setTag(tag).setStreamKeys(streamKeys).build();
} else if (needsTag) {
mediaItem = mediaItem.buildUpon().setTag(tag).build();
} else if (needsStreamKeys) {
mediaItem = mediaItem.buildUpon().setStreamKeys(streamKeys).build();
boolean needsTargetLiveOffset =
mediaItem.liveConfiguration.targetLiveOffsetMs == C.TIME_UNSET
&& targetLiveOffsetOverrideMs != C.TIME_UNSET;
if (needsTag || needsStreamKeys || needsTargetLiveOffset) {
MediaItem.Builder builder = mediaItem.buildUpon();
if (needsTag) {
builder.setTag(tag);
}
if (needsStreamKeys) {
builder.setStreamKeys(streamKeys);
}
if (needsTargetLiveOffset) {
builder.setLiveTargetOffsetMs(targetLiveOffsetOverrideMs);
}
mediaItem = builder.build();
}
return new DashMediaSource(
mediaItem,
@ -381,8 +394,7 @@ public final class DashMediaSource extends BaseMediaSource {
compositeSequenceableLoaderFactory,
drmSessionManager != null ? drmSessionManager : mediaSourceDrmHelper.create(mediaItem),
loadErrorHandlingPolicy,
livePresentationDelayMs,
livePresentationDelayOverridesManifest);
fallbackTargetLiveOffsetMs);
}
@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
* which the default start position precedes the end of the live window.
* The default target {@link Player#getCurrentLiveOffset() offset for live streams} that is used
* if no value is defined in the {@link MediaItem} or the manifest.
*/
public static final long DEFAULT_LIVE_PRESENTATION_DELAY_MS = 30_000;
/** @deprecated Use {@link #DEFAULT_LIVE_PRESENTATION_DELAY_MS}. */
@Deprecated
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;
public static final long DEFAULT_FALLBACK_TARGET_LIVE_OFFSET_MS = 30_000;
/** @deprecated Use {@link #DEFAULT_FALLBACK_TARGET_LIVE_OFFSET_MS} instead. */
@Deprecated public static final long DEFAULT_LIVE_PRESENTATION_DELAY_MS = 30_000;
/** The media id used by media items of dash media sources without a manifest URI. */
public static final String DUMMY_MEDIA_ID =
"com.google.android.exoplayer2.source.dash.DashMediaSource";
@ -426,8 +433,7 @@ public final class DashMediaSource extends BaseMediaSource {
private final CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory;
private final DrmSessionManager drmSessionManager;
private final LoadErrorHandlingPolicy loadErrorHandlingPolicy;
private final long livePresentationDelayMs;
private final boolean livePresentationDelayOverridesManifest;
private final long fallbackTargetLiveOffsetMs;
private final EventDispatcher manifestEventDispatcher;
private final ParsingLoadable.Parser<? extends DashManifest> manifestParser;
private final ManifestCallback manifestCallback;
@ -437,8 +443,6 @@ public final class DashMediaSource extends BaseMediaSource {
private final Runnable simulateManifestRefreshRunnable;
private final PlayerEmsgCallback playerEmsgCallback;
private final LoaderErrorThrower manifestLoadErrorThrower;
private final MediaItem mediaItem;
private final MediaItem.PlaybackProperties playbackProperties;
private DataSource dataSource;
private Loader loader;
@ -447,6 +451,8 @@ public final class DashMediaSource extends BaseMediaSource {
private IOException manifestFatalError;
private Handler handler;
private MediaItem mediaItem;
private MediaItem.PlaybackProperties playbackProperties;
private Uri manifestUri;
private Uri initialManifestUri;
private DashManifest manifest;
@ -469,8 +475,7 @@ public final class DashMediaSource extends BaseMediaSource {
CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory,
DrmSessionManager drmSessionManager,
LoadErrorHandlingPolicy loadErrorHandlingPolicy,
long livePresentationDelayMs,
boolean livePresentationDelayOverridesManifest) {
long fallbackTargetLiveOffsetMs) {
this.mediaItem = mediaItem;
this.playbackProperties = checkNotNull(mediaItem.playbackProperties);
this.manifestUri = playbackProperties.uri;
@ -481,8 +486,7 @@ public final class DashMediaSource extends BaseMediaSource {
this.chunkSourceFactory = chunkSourceFactory;
this.drmSessionManager = drmSessionManager;
this.loadErrorHandlingPolicy = loadErrorHandlingPolicy;
this.livePresentationDelayMs = livePresentationDelayMs;
this.livePresentationDelayOverridesManifest = livePresentationDelayOverridesManifest;
this.fallbackTargetLiveOffsetMs = fallbackTargetLiveOffsetMs;
this.compositeSequenceableLoaderFactory = compositeSequenceableLoaderFactory;
sideloadedManifest = manifest != null;
manifestEventDispatcher = createEventDispatcher(/* mediaPeriodId= */ null);
@ -688,6 +692,9 @@ public final class DashMediaSource extends BaseMediaSource {
staleManifestReloadAttempt = 0;
}
mediaItem = mergeLiveConfiguration(mediaItem, fallbackTargetLiveOffsetMs, newManifest);
playbackProperties = castNonNull(mediaItem.playbackProperties);
manifest = newManifest;
manifestLoadPending &= manifest.dynamic;
manifestLoadStartTimestampMs = elapsedRealtimeMs - loadDurationMs;
@ -921,23 +928,7 @@ public final class DashMediaSource extends BaseMediaSource {
for (int i = 0; i < manifest.getPeriodCount() - 1; 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;
if (manifest.availabilityStartTimeMs != C.TIME_UNSET) {
windowStartTimeMs =
@ -945,6 +936,25 @@ public final class DashMediaSource extends BaseMediaSource {
+ manifest.getPeriod(0).startMs
+ 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 =
new DashTimeline(
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) {
handler.postDelayed(refreshManifestRunnable, delayUntilNextLoadMs);
}
@ -1057,6 +1087,41 @@ public final class DashMediaSource extends BaseMediaSource {
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 {
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
// not correspond to the start of a segment in both, but this is an edge case.
DashSegmentIndex snapIndex = period.adaptationSets.get(videoAdaptationSetIndex)
.representations.get(0).getIndex();
@Nullable
DashSegmentIndex snapIndex =
period.adaptationSets.get(videoAdaptationSetIndex).representations.get(0).getIndex();
if (snapIndex == null || snapIndex.getSegmentCount(periodDurationUs) == 0) {
// Video adaptation set does not include a non-empty index for snapping.
return windowDefaultStartPositionUs;

View File

@ -16,27 +16,56 @@
package com.google.android.exoplayer2.source.dash;
import static com.google.common.truth.Truth.assertThat;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static org.junit.Assert.fail;
import static org.robolectric.annotation.LooperMode.Mode.PAUSED;
import android.net.Uri;
import androidx.annotation.Nullable;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.MediaItem;
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.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.ParsingLoadable;
import com.google.android.exoplayer2.util.Util;
import com.google.common.collect.ImmutableList;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicReference;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.annotation.LooperMode;
import org.robolectric.shadows.ShadowLooper;
/** Unit test for {@link DashMediaSource}. */
@RunWith(AndroidJUnit4.class)
@LooperMode(PAUSED)
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
public void iso8601ParserParse() throws IOException {
DashMediaSource.Iso8601Parser parser = new DashMediaSource.Iso8601Parser();
@ -157,7 +186,7 @@ public final class DashMediaSourceTest {
// Tests backwards compatibility
@SuppressWarnings("deprecation")
@Test
public void factorySetStreamKeys_withMediaItemStreamKeys_doesNotsOverrideMediaItemStreamKeys() {
public void factorySetStreamKeys_withMediaItemStreamKeys_doesNotOverrideMediaItemStreamKeys() {
StreamKey mediaItemStreamKey = new StreamKey(/* groupIndex= */ 0, /* trackIndex= */ 1);
MediaItem mediaItem =
new MediaItem.Builder()
@ -187,6 +216,280 @@ public final class DashMediaSourceTest {
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(
long expected, ParsingLoadable.Parser<Long> parser, String data) throws IOException {
long actual = parser.parse(null, new ByteArrayInputStream(Util.getUtf8Bytes(data)));

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>