From 41b58d503ae6666cea4ded114afde9fb23a5e199 Mon Sep 17 00:00:00 2001 From: christosts Date: Wed, 14 Oct 2020 10:13:10 +0100 Subject: [PATCH] Use low latency properties in DashMediaSource Issue: #4904 PiperOrigin-RevId: 337046645 --- .../source/dash/DashMediaSource.java | 206 ++++++++---- .../source/dash/DashMediaSourceTest.java | 305 +++++++++++++++++- ...mpd_live_with_complete_service_description | 27 ++ .../sample_mpd_live_with_offset_inside_window | 24 ++ .../mpd/sample_mpd_live_with_offset_too_long | 24 ++ .../mpd/sample_mpd_live_with_offset_too_short | 25 ++ ..._live_with_suggested_presentation_delay_2s | 23 ++ ...sample_mpd_live_without_live_configuration | 22 ++ 8 files changed, 585 insertions(+), 71 deletions(-) create mode 100644 testdata/src/test/assets/media/mpd/sample_mpd_live_with_complete_service_description create mode 100644 testdata/src/test/assets/media/mpd/sample_mpd_live_with_offset_inside_window create mode 100644 testdata/src/test/assets/media/mpd/sample_mpd_live_with_offset_too_long create mode 100644 testdata/src/test/assets/media/mpd/sample_mpd_live_with_offset_too_short create mode 100644 testdata/src/test/assets/media/mpd/sample_mpd_live_with_suggested_presentation_delay_2s create mode 100644 testdata/src/test/assets/media/mpd/sample_mpd_live_without_live_configuration diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java index 683da2d0c4..b40972c830 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashMediaSource.java @@ -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 manifestParser; private List 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. + *

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 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; diff --git a/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/DashMediaSourceTest.java b/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/DashMediaSourceTest.java index aa65237095..a4b3c2c562 100644 --- a/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/DashMediaSourceTest.java +++ b/library/dash/src/test/java/com/google/android/exoplayer2/source/dash/DashMediaSourceTest.java @@ -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 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 parser, String data) throws IOException { long actual = parser.parse(null, new ByteArrayInputStream(Util.getUtf8Bytes(data))); diff --git a/testdata/src/test/assets/media/mpd/sample_mpd_live_with_complete_service_description b/testdata/src/test/assets/media/mpd/sample_mpd_live_with_complete_service_description new file mode 100644 index 0000000000..f0e6e61255 --- /dev/null +++ b/testdata/src/test/assets/media/mpd/sample_mpd_live_with_complete_service_description @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + diff --git a/testdata/src/test/assets/media/mpd/sample_mpd_live_with_offset_inside_window b/testdata/src/test/assets/media/mpd/sample_mpd_live_with_offset_inside_window new file mode 100644 index 0000000000..e0d4cfdddc --- /dev/null +++ b/testdata/src/test/assets/media/mpd/sample_mpd_live_with_offset_inside_window @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + diff --git a/testdata/src/test/assets/media/mpd/sample_mpd_live_with_offset_too_long b/testdata/src/test/assets/media/mpd/sample_mpd_live_with_offset_too_long new file mode 100644 index 0000000000..8419c505e1 --- /dev/null +++ b/testdata/src/test/assets/media/mpd/sample_mpd_live_with_offset_too_long @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + diff --git a/testdata/src/test/assets/media/mpd/sample_mpd_live_with_offset_too_short b/testdata/src/test/assets/media/mpd/sample_mpd_live_with_offset_too_short new file mode 100644 index 0000000000..3e09d344c2 --- /dev/null +++ b/testdata/src/test/assets/media/mpd/sample_mpd_live_with_offset_too_short @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + diff --git a/testdata/src/test/assets/media/mpd/sample_mpd_live_with_suggested_presentation_delay_2s b/testdata/src/test/assets/media/mpd/sample_mpd_live_with_suggested_presentation_delay_2s new file mode 100644 index 0000000000..e79977ba79 --- /dev/null +++ b/testdata/src/test/assets/media/mpd/sample_mpd_live_with_suggested_presentation_delay_2s @@ -0,0 +1,23 @@ + + + + + + + + + + + + diff --git a/testdata/src/test/assets/media/mpd/sample_mpd_live_without_live_configuration b/testdata/src/test/assets/media/mpd/sample_mpd_live_without_live_configuration new file mode 100644 index 0000000000..96ffdd1dd5 --- /dev/null +++ b/testdata/src/test/assets/media/mpd/sample_mpd_live_without_live_configuration @@ -0,0 +1,22 @@ + + + + + + + + + + + +