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

View File

@ -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)));

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>