diff --git a/RELEASENOTES.md b/RELEASENOTES.md index e5c67f3a36..1704847087 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -17,7 +17,7 @@ * Sleep and retry when creating a `MediaCodec` instance fails. This works around an issue that occurs on some devices when switching a surface from a secure codec to another codec - (#8696)[https://github.com/google/ExoPlayer/issues/8696]. + ((#8696)[https://github.com/google/ExoPlayer/issues/8696]). * Add `MediaCodecAdapter.getMetrics()` to allow users obtain metrics data from `MediaCodec`. ([#9766](https://github.com/google/ExoPlayer/issues/9766)). @@ -28,6 +28,9 @@ ((#8353)[https://github.com/google/ExoPlayer/issues/8353]). * Fix decoder fallback logic for Dolby Atmos (E-AC3-JOC) and Dolby Vision to use a compatible base decoder (E-AC3 or H264/H265) if needed. + * Disable automatic speed adjustment for live streams that neither have + low-latency features nor a user request setting the speed + ((#9329)[https://github.com/google/ExoPlayer/issues/9329]). * Android 12 compatibility: * Upgrade the Cast extension to depend on `com.google.android.gms:play-services-cast-framework:20.1.0`. Earlier diff --git a/library/core/src/main/java/com/google/android/exoplayer2/DefaultLivePlaybackSpeedControl.java b/library/core/src/main/java/com/google/android/exoplayer2/DefaultLivePlaybackSpeedControl.java index 59753448db..26ef315742 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/DefaultLivePlaybackSpeedControl.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/DefaultLivePlaybackSpeedControl.java @@ -308,6 +308,10 @@ public final class DefaultLivePlaybackSpeedControl implements LivePlaybackSpeedC liveConfiguration.maxPlaybackSpeed != C.RATE_UNSET ? liveConfiguration.maxPlaybackSpeed : fallbackMaxPlaybackSpeed; + if (minPlaybackSpeed == 1f && maxPlaybackSpeed == 1f) { + // Don't bother calculating adjustments if it's not possible to change the speed. + mediaConfigurationTargetLiveOffsetUs = C.TIME_UNSET; + } maybeResetTargetLiveOffsetUs(); } diff --git a/library/core/src/test/java/com/google/android/exoplayer2/DefaultLivePlaybackSpeedControlTest.java b/library/core/src/test/java/com/google/android/exoplayer2/DefaultLivePlaybackSpeedControlTest.java index cc05a87558..016e886f6e 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/DefaultLivePlaybackSpeedControlTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/DefaultLivePlaybackSpeedControlTest.java @@ -48,8 +48,8 @@ public class DefaultLivePlaybackSpeedControlTest { .setTargetOffsetMs(42) .setMinOffsetMs(5) .setMaxOffsetMs(400) - .setMinPlaybackSpeed(1f) - .setMaxPlaybackSpeed(1f) + .setMinPlaybackSpeed(0.95f) + .setMaxPlaybackSpeed(1.05f) .build()); assertThat(defaultLivePlaybackSpeedControl.getTargetLiveOffsetUs()).isEqualTo(42_000); @@ -65,8 +65,8 @@ public class DefaultLivePlaybackSpeedControlTest { .setTargetOffsetMs(4321) .setMinOffsetMs(5) .setMaxOffsetMs(400) - .setMinPlaybackSpeed(1f) - .setMaxPlaybackSpeed(1f) + .setMinPlaybackSpeed(0.95f) + .setMaxPlaybackSpeed(1.05f) .build()); assertThat(defaultLivePlaybackSpeedControl.getTargetLiveOffsetUs()).isEqualTo(400_000); @@ -82,8 +82,8 @@ public class DefaultLivePlaybackSpeedControlTest { .setTargetOffsetMs(3) .setMinOffsetMs(5) .setMaxOffsetMs(400) - .setMinPlaybackSpeed(1f) - .setMaxPlaybackSpeed(1f) + .setMinPlaybackSpeed(0.95f) + .setMaxPlaybackSpeed(1.05f) .build()); assertThat(defaultLivePlaybackSpeedControl.getTargetLiveOffsetUs()).isEqualTo(5_000); @@ -100,8 +100,8 @@ public class DefaultLivePlaybackSpeedControlTest { .setTargetOffsetMs(42) .setMinOffsetMs(5) .setMaxOffsetMs(400) - .setMinPlaybackSpeed(1f) - .setMaxPlaybackSpeed(1f) + .setMinPlaybackSpeed(0.95f) + .setMaxPlaybackSpeed(1.05f) .build()); long targetLiveOffsetUs = defaultLivePlaybackSpeedControl.getTargetLiveOffsetUs(); @@ -121,8 +121,8 @@ public class DefaultLivePlaybackSpeedControlTest { .setTargetOffsetMs(42) .setMinOffsetMs(5) .setMaxOffsetMs(400) - .setMinPlaybackSpeed(1f) - .setMaxPlaybackSpeed(1f) + .setMinPlaybackSpeed(0.95f) + .setMaxPlaybackSpeed(1.05f) .build()); long targetLiveOffsetUs = defaultLivePlaybackSpeedControl.getTargetLiveOffsetUs(); @@ -142,8 +142,8 @@ public class DefaultLivePlaybackSpeedControlTest { .setTargetOffsetMs(42) .setMinOffsetMs(5) .setMaxOffsetMs(400) - .setMinPlaybackSpeed(1f) - .setMaxPlaybackSpeed(1f) + .setMinPlaybackSpeed(0.95f) + .setMaxPlaybackSpeed(1.05f) .build()); long targetLiveOffsetUs = defaultLivePlaybackSpeedControl.getTargetLiveOffsetUs(); @@ -163,6 +163,22 @@ public class DefaultLivePlaybackSpeedControlTest { assertThat(targetLiveOffsetUs).isEqualTo(C.TIME_UNSET); } + @Test + public void getTargetLiveOffsetUs_withUnitSpeed_returnsUnset() { + DefaultLivePlaybackSpeedControl defaultLivePlaybackSpeedControl = + new DefaultLivePlaybackSpeedControl.Builder().build(); + defaultLivePlaybackSpeedControl.setLiveConfiguration( + new LiveConfiguration.Builder() + .setTargetOffsetMs(42) + .setMinPlaybackSpeed(1f) + .setMaxPlaybackSpeed(1f) + .build()); + + long targetLiveOffsetUs = defaultLivePlaybackSpeedControl.getTargetLiveOffsetUs(); + + assertThat(targetLiveOffsetUs).isEqualTo(C.TIME_UNSET); + } + @Test public void getTargetLiveOffsetUs_afterSetTargetLiveOffsetOverrideWithTimeUnset_returnsMediaLiveOffset() { @@ -174,8 +190,8 @@ public class DefaultLivePlaybackSpeedControlTest { .setTargetOffsetMs(42) .setMinOffsetMs(5) .setMaxOffsetMs(400) - .setMinPlaybackSpeed(1f) - .setMaxPlaybackSpeed(1f) + .setMinPlaybackSpeed(0.95f) + .setMaxPlaybackSpeed(1.05f) .build()); defaultLivePlaybackSpeedControl.setTargetLiveOffsetOverrideUs(C.TIME_UNSET); @@ -195,8 +211,8 @@ public class DefaultLivePlaybackSpeedControlTest { .setTargetOffsetMs(42) .setMinOffsetMs(5) .setMaxOffsetMs(400) - .setMinPlaybackSpeed(1f) - .setMaxPlaybackSpeed(1f) + .setMinPlaybackSpeed(0.95f) + .setMaxPlaybackSpeed(1.05f) .build()); long targetLiveOffsetBeforeUs = defaultLivePlaybackSpeedControl.getTargetLiveOffsetUs(); @@ -218,8 +234,8 @@ public class DefaultLivePlaybackSpeedControlTest { .setTargetOffsetMs(42) .setMinOffsetMs(5) .setMaxOffsetMs(400) - .setMinPlaybackSpeed(1f) - .setMaxPlaybackSpeed(1f) + .setMinPlaybackSpeed(0.95f) + .setMaxPlaybackSpeed(1.05f) .build()); List targetOffsetsUs = new ArrayList<>(); @@ -244,8 +260,8 @@ public class DefaultLivePlaybackSpeedControlTest { .setTargetOffsetMs(42) .setMinOffsetMs(5) .setMaxOffsetMs(400) - .setMinPlaybackSpeed(1f) - .setMaxPlaybackSpeed(1f) + .setMinPlaybackSpeed(0.95f) + .setMaxPlaybackSpeed(1.05f) .build()); defaultLivePlaybackSpeedControl.notifyRebuffer(); @@ -266,8 +282,8 @@ public class DefaultLivePlaybackSpeedControlTest { .setTargetOffsetMs(42) .setMinOffsetMs(5) .setMaxOffsetMs(400) - .setMinPlaybackSpeed(1f) - .setMaxPlaybackSpeed(1f) + .setMinPlaybackSpeed(0.95f) + .setMaxPlaybackSpeed(1.05f) .build()); defaultLivePlaybackSpeedControl.notifyRebuffer(); @@ -289,8 +305,8 @@ public class DefaultLivePlaybackSpeedControlTest { .setTargetOffsetMs(42) .setMinOffsetMs(5) .setMaxOffsetMs(400) - .setMinPlaybackSpeed(1f) - .setMaxPlaybackSpeed(1f) + .setMinPlaybackSpeed(0.95f) + .setMaxPlaybackSpeed(1.05f) .build()); long targetLiveOffsetBeforeUs = defaultLivePlaybackSpeedControl.getTargetLiveOffsetUs(); @@ -321,8 +337,8 @@ public class DefaultLivePlaybackSpeedControlTest { .setTargetOffsetMs(42) .setMinOffsetMs(5) .setMaxOffsetMs(400) - .setMinPlaybackSpeed(1f) - .setMaxPlaybackSpeed(1f) + .setMinPlaybackSpeed(0.95f) + .setMaxPlaybackSpeed(1.05f) .build()); defaultLivePlaybackSpeedControl.notifyRebuffer(); 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 92f03e29f5..868fe4afce 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 @@ -800,7 +800,7 @@ public final class DashMediaSource extends BaseMediaSource { nowUnixTimeUs - Util.msToUs(manifest.availabilityStartTimeMs) - windowStartTimeInManifestUs; - updateMediaItemLiveConfiguration(nowInWindowUs, windowDurationUs); + updateLiveConfiguration(nowInWindowUs, windowDurationUs); windowStartUnixTimeMs = manifest.availabilityStartTimeMs + Util.usToMs(windowStartTimeInManifestUs); windowDefaultPositionUs = nowInWindowUs - Util.msToUs(liveConfiguration.targetOffsetMs); @@ -859,7 +859,7 @@ public final class DashMediaSource extends BaseMediaSource { } } - private void updateMediaItemLiveConfiguration(long nowInWindowUs, long windowDurationUs) { + private void updateLiveConfiguration(long nowInWindowUs, long windowDurationUs) { // Default maximum offset: start of window. long maxPossibleLiveOffsetMs = usToMs(nowInWindowUs); long maxLiveOffsetMs = maxPossibleLiveOffsetMs; @@ -934,6 +934,16 @@ public final class DashMediaSource extends BaseMediaSource { } else if (manifest.serviceDescription != null) { maxPlaybackSpeed = manifest.serviceDescription.maxPlaybackSpeed; } + if (minPlaybackSpeed == C.RATE_UNSET + && maxPlaybackSpeed == C.RATE_UNSET + && (manifest.serviceDescription == null + || manifest.serviceDescription.targetOffsetMs == C.TIME_UNSET)) { + // Force unit speed (instead of automatic adjustment with fallback speeds) if there are no + // specific speed limits defined by the media item or the manifest, and the manifest contains + // no low-latency target offset either. + minPlaybackSpeed = 1f; + maxPlaybackSpeed = 1f; + } liveConfiguration = new MediaItem.LiveConfiguration.Builder() .setTargetOffsetMs(targetOffsetMs) 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 bebcb074ca..9674453f81 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 @@ -24,6 +24,7 @@ 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.MediaItem.LiveConfiguration; import com.google.android.exoplayer2.ParserException; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.Timeline.Window; @@ -140,7 +141,7 @@ public final class DashMediaSourceTest { } @Test - public void prepare_withoutLiveConfiguration_withoutMediaItemLiveProperties_usesDefaultFallback() + public void prepare_withoutLiveConfiguration_withoutMediaItemLiveConfiguration_usesUnitSpeed() throws InterruptedException { DashMediaSource mediaSource = new DashMediaSource.Factory( @@ -154,18 +155,71 @@ public final class DashMediaSourceTest { .isEqualTo(DashMediaSource.DEFAULT_FALLBACK_TARGET_LIVE_OFFSET_MS); assertThat(liveConfiguration.minOffsetMs).isEqualTo(0L); assertThat(liveConfiguration.maxOffsetMs).isEqualTo(58_000L); - assertThat(liveConfiguration.minPlaybackSpeed).isEqualTo(C.RATE_UNSET); - assertThat(liveConfiguration.maxPlaybackSpeed).isEqualTo(C.RATE_UNSET); + assertThat(liveConfiguration.minPlaybackSpeed).isEqualTo(1f); + assertThat(liveConfiguration.maxPlaybackSpeed).isEqualTo(1f); } @Test - public void prepare_withoutLiveConfiguration_withoutMediaItemLiveProperties_usesFallback() + public void prepare_withoutLiveConfiguration_withOnlyMediaItemTargetOffset_usesUnitSpeed() throws InterruptedException { + DashMediaSource mediaSource = + new DashMediaSource.Factory( + () -> createSampleMpdDataSource(SAMPLE_MPD_LIVE_WITHOUT_LIVE_CONFIGURATION)) + .createMediaSource( + new MediaItem.Builder() + .setUri(Uri.EMPTY) + .setLiveConfiguration( + new LiveConfiguration.Builder().setTargetOffsetMs(10_000L).build()) + .build()); + + MediaItem.LiveConfiguration liveConfiguration = + prepareAndWaitForTimelineRefresh(mediaSource).liveConfiguration; + + assertThat(liveConfiguration.targetOffsetMs).isEqualTo(10_000L); + assertThat(liveConfiguration.minOffsetMs).isEqualTo(0L); + assertThat(liveConfiguration.maxOffsetMs).isEqualTo(58_000L); + assertThat(liveConfiguration.minPlaybackSpeed).isEqualTo(1f); + assertThat(liveConfiguration.maxPlaybackSpeed).isEqualTo(1f); + } + + @Test + public void prepare_withoutLiveConfiguration_withMediaItemSpeedLimits_usesDefaultFallbackValues() + throws InterruptedException { + DashMediaSource mediaSource = + new DashMediaSource.Factory( + () -> createSampleMpdDataSource(SAMPLE_MPD_LIVE_WITHOUT_LIVE_CONFIGURATION)) + .createMediaSource( + new MediaItem.Builder() + .setUri(Uri.EMPTY) + .setLiveConfiguration( + new LiveConfiguration.Builder().setMinPlaybackSpeed(0.95f).build()) + .build()); + + MediaItem.LiveConfiguration liveConfiguration = + prepareAndWaitForTimelineRefresh(mediaSource).liveConfiguration; + + assertThat(liveConfiguration.targetOffsetMs) + .isEqualTo(DashMediaSource.DEFAULT_FALLBACK_TARGET_LIVE_OFFSET_MS); + assertThat(liveConfiguration.minOffsetMs).isEqualTo(0L); + assertThat(liveConfiguration.maxOffsetMs).isEqualTo(58_000L); + assertThat(liveConfiguration.minPlaybackSpeed).isEqualTo(0.95f); + assertThat(liveConfiguration.maxPlaybackSpeed).isEqualTo(C.RATE_UNSET); + } + + @Test + public void + prepare_withoutLiveConfiguration_withoutMediaItemTargetOffset_usesDefinedFallbackTargetOffset() + throws InterruptedException { DashMediaSource mediaSource = new DashMediaSource.Factory( () -> createSampleMpdDataSource(SAMPLE_MPD_LIVE_WITHOUT_LIVE_CONFIGURATION)) .setFallbackTargetLiveOffsetMs(1234L) - .createMediaSource(MediaItem.fromUri(Uri.EMPTY)); + .createMediaSource( + new MediaItem.Builder() + .setUri(Uri.EMPTY) + .setLiveConfiguration( + new LiveConfiguration.Builder().setMinPlaybackSpeed(0.95f).build()) + .build()); MediaItem.LiveConfiguration liveConfiguration = prepareAndWaitForTimelineRefresh(mediaSource).liveConfiguration; @@ -173,7 +227,7 @@ public final class DashMediaSourceTest { assertThat(liveConfiguration.targetOffsetMs).isEqualTo(1234L); assertThat(liveConfiguration.minOffsetMs).isEqualTo(0L); assertThat(liveConfiguration.maxOffsetMs).isEqualTo(58_000L); - assertThat(liveConfiguration.minPlaybackSpeed).isEqualTo(C.RATE_UNSET); + assertThat(liveConfiguration.minPlaybackSpeed).isEqualTo(0.95f); assertThat(liveConfiguration.maxPlaybackSpeed).isEqualTo(C.RATE_UNSET); } @@ -213,7 +267,12 @@ public final class DashMediaSourceTest { createSampleMpdDataSource( SAMPLE_MPD_LIVE_WITH_SUGGESTED_PRESENTATION_DELAY_2S_MIN_BUFFER_TIME_500MS)) .setFallbackTargetLiveOffsetMs(1234L) - .createMediaSource(MediaItem.fromUri(Uri.EMPTY)); + .createMediaSource( + new MediaItem.Builder() + .setUri(Uri.EMPTY) + .setLiveConfiguration( + new LiveConfiguration.Builder().setMaxPlaybackSpeed(1.05f).build()) + .build()); MediaItem.LiveConfiguration liveConfiguration = prepareAndWaitForTimelineRefresh(mediaSource).liveConfiguration; @@ -222,7 +281,7 @@ public final class DashMediaSourceTest { assertThat(liveConfiguration.minOffsetMs).isEqualTo(500L); assertThat(liveConfiguration.maxOffsetMs).isEqualTo(58_000L); assertThat(liveConfiguration.minPlaybackSpeed).isEqualTo(C.RATE_UNSET); - assertThat(liveConfiguration.maxPlaybackSpeed).isEqualTo(C.RATE_UNSET); + assertThat(liveConfiguration.maxPlaybackSpeed).isEqualTo(1.05f); } @Test diff --git a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaSource.java b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaSource.java index b6028b7649..e7143ffd27 100644 --- a/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaSource.java +++ b/library/hls/src/main/java/com/google/android/exoplayer2/source/hls/HlsMediaSource.java @@ -26,6 +26,7 @@ import androidx.annotation.VisibleForTesting; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlayerLibraryInfo; import com.google.android.exoplayer2.MediaItem; +import com.google.android.exoplayer2.MediaItem.LiveConfiguration; import com.google.android.exoplayer2.drm.DefaultDrmSessionManagerProvider; import com.google.android.exoplayer2.drm.DrmSessionEventListener; import com.google.android.exoplayer2.drm.DrmSessionManager; @@ -474,7 +475,7 @@ public final class HlsMediaSource extends BaseMediaSource targetLiveOffsetUs = Util.constrainValue( targetLiveOffsetUs, liveEdgeOffsetUs, playlist.durationUs + liveEdgeOffsetUs); - maybeUpdateLiveConfiguration(targetLiveOffsetUs); + updateLiveConfiguration(playlist, targetLiveOffsetUs); long windowDefaultStartPositionUs = getLiveWindowDefaultStartPositionUs(playlist, liveEdgeOffsetUs); boolean suppressPositionProjection = @@ -564,12 +565,18 @@ public final class HlsMediaSource extends BaseMediaSource return segment.relativeStartTimeUs; } - private void maybeUpdateLiveConfiguration(long targetLiveOffsetUs) { - long targetLiveOffsetMs = Util.usToMs(targetLiveOffsetUs); - if (targetLiveOffsetMs != liveConfiguration.targetOffsetMs) { - liveConfiguration = - liveConfiguration.buildUpon().setTargetOffsetMs(targetLiveOffsetMs).build(); - } + private void updateLiveConfiguration(HlsMediaPlaylist playlist, long targetLiveOffsetUs) { + boolean disableSpeedAdjustment = + mediaItem.liveConfiguration.minPlaybackSpeed == C.RATE_UNSET + && mediaItem.liveConfiguration.maxPlaybackSpeed == C.RATE_UNSET + && playlist.serverControl.holdBackUs == C.TIME_UNSET + && playlist.serverControl.partHoldBackUs == C.TIME_UNSET; + liveConfiguration = + new LiveConfiguration.Builder() + .setTargetOffsetMs(Util.usToMs(targetLiveOffsetUs)) + .setMinPlaybackSpeed(disableSpeedAdjustment ? 1f : liveConfiguration.minPlaybackSpeed) + .setMaxPlaybackSpeed(disableSpeedAdjustment ? 1f : liveConfiguration.maxPlaybackSpeed) + .build(); } /** diff --git a/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/HlsMediaSourceTest.java b/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/HlsMediaSourceTest.java index 9555fb73b4..9e4ced67f2 100644 --- a/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/HlsMediaSourceTest.java +++ b/library/hls/src/test/java/com/google/android/exoplayer2/source/hls/HlsMediaSourceTest.java @@ -21,6 +21,7 @@ import static com.google.common.truth.Truth.assertThat; import android.net.Uri; import android.os.SystemClock; 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; @@ -77,6 +78,8 @@ public class HlsMediaSourceTest { // The target live offset is picked from target duration (3 * 4 = 12 seconds) and then expressed // in relation to the live edge (12 + 1 seconds). assertThat(window.liveConfiguration.targetOffsetMs).isEqualTo(13000); + assertThat(window.liveConfiguration.minPlaybackSpeed).isEqualTo(1f); + assertThat(window.liveConfiguration.maxPlaybackSpeed).isEqualTo(1f); assertThat(window.defaultPositionUs).isEqualTo(4000000); } @@ -100,7 +103,7 @@ public class HlsMediaSourceTest { + "#EXTINF:4.00000,\n" + "fileSequence3.ts\n" + "#EXT-X-SERVER-CONTROL:HOLD-BACK=12"; - // The playlist finishes 1 second before the the current time, therefore there's a live edge + // The playlist finishes 1 second before the current time, therefore there's a live edge // offset of 1 second. SystemClock.setCurrentTimeMillis(Util.parseXsDateTime("2020-01-01T00:00:17.0+00:00")); HlsMediaSource.Factory factory = createHlsMediaSourceFactory(playlistUri, playlist); @@ -113,6 +116,8 @@ public class HlsMediaSourceTest { // The target live offset is picked from hold back and then expressed in relation to the live // edge (+1 seconds). assertThat(window.liveConfiguration.targetOffsetMs).isEqualTo(13000); + assertThat(window.liveConfiguration.minPlaybackSpeed).isEqualTo(C.RATE_UNSET); + assertThat(window.liveConfiguration.maxPlaybackSpeed).isEqualTo(C.RATE_UNSET); assertThat(window.defaultPositionUs).isEqualTo(4000000); } @@ -139,7 +144,7 @@ public class HlsMediaSourceTest { + "#EXTINF:4.00000,\n" + "fileSequence3.ts\n" + "#EXT-X-SERVER-CONTROL:HOLD-BACK=12,PART-HOLD-BACK=3"; - // The playlist finishes 1 second before the the current time. + // The playlist finishes 1 second before the current time. SystemClock.setCurrentTimeMillis(Util.parseXsDateTime("2020-01-01T00:00:17.0+00:00")); HlsMediaSource.Factory factory = createHlsMediaSourceFactory(playlistUri, playlist); MediaItem mediaItem = MediaItem.fromUri(playlistUri); @@ -151,6 +156,8 @@ public class HlsMediaSourceTest { // The target live offset is picked from hold back and then expressed in relation to the live // edge (+1 seconds). assertThat(window.liveConfiguration.targetOffsetMs).isEqualTo(13000); + assertThat(window.liveConfiguration.minPlaybackSpeed).isEqualTo(C.RATE_UNSET); + assertThat(window.liveConfiguration.maxPlaybackSpeed).isEqualTo(C.RATE_UNSET); assertThat(window.defaultPositionUs).isEqualTo(4000000); } @@ -182,6 +189,8 @@ public class HlsMediaSourceTest { // The target live offset is picked from part hold back and then expressed in relation to the // live edge (+1 seconds). assertThat(window.liveConfiguration.targetOffsetMs).isEqualTo(4000); + assertThat(window.liveConfiguration.minPlaybackSpeed).isEqualTo(C.RATE_UNSET); + assertThat(window.liveConfiguration.maxPlaybackSpeed).isEqualTo(C.RATE_UNSET); assertThat(window.defaultPositionUs).isEqualTo(0); } @@ -395,7 +404,7 @@ public class HlsMediaSourceTest { + "#EXTINF:4.00000,\n" + "fileSequence0.ts\n" + "#EXT-X-SERVER-CONTROL:HOLD-BACK=12,PART-HOLD-BACK=3"; - // The playlist finishes 1 second before the the current time. This should not affect the target + // The playlist finishes 1 second before the current time. This should not affect the target // live offset set in the media item. SystemClock.setCurrentTimeMillis(Util.parseXsDateTime("2020-01-01T00:00:05.0+00:00")); HlsMediaSource.Factory factory = createHlsMediaSourceFactory(playlistUri, playlist); @@ -415,6 +424,76 @@ public class HlsMediaSourceTest { assertThat(window.defaultPositionUs).isEqualTo(0); } + @Test + public void loadLivePlaylist_noHoldBackInPlaylistAndNoPlaybackSpeedInMediaItem_usesUnitSpeed() + throws TimeoutException, ParserException { + String playlistUri = "fake://foo.bar/media0/playlist.m3u8"; + // The playlist has no hold back defined. + String playlist = + "#EXTM3U\n" + + "#EXT-X-PROGRAM-DATE-TIME:2020-01-01T00:00:00.0+00:00\n" + + "#EXT-X-TARGETDURATION:4\n" + + "#EXT-X-VERSION:3\n" + + "#EXT-X-MEDIA-SEQUENCE:0\n" + + "#EXTINF:4.00000,\n" + + "fileSequence0.ts"; + // The playlist finishes 1 second before the current time. This should not affect the target + // live offset set in the media item. + SystemClock.setCurrentTimeMillis(Util.parseXsDateTime("2020-01-01T00:00:05.0+00:00")); + HlsMediaSource.Factory factory = createHlsMediaSourceFactory(playlistUri, playlist); + MediaItem mediaItem = + new MediaItem.Builder() + .setUri(playlistUri) + .setLiveConfiguration( + new MediaItem.LiveConfiguration.Builder().setTargetOffsetMs(1000).build()) + .build(); + HlsMediaSource mediaSource = factory.createMediaSource(mediaItem); + + Timeline timeline = prepareAndWaitForTimeline(mediaSource); + + Timeline.Window window = timeline.getWindow(0, new Timeline.Window()); + assertThat(window.liveConfiguration.minPlaybackSpeed).isEqualTo(1f); + assertThat(window.liveConfiguration.maxPlaybackSpeed).isEqualTo(1f); + assertThat(window.defaultPositionUs).isEqualTo(0); + } + + @Test + public void + loadLivePlaylist_noHoldBackInPlaylistAndPlaybackSpeedInMediaItem_usesMediaItemConfiguration() + throws TimeoutException, ParserException { + String playlistUri = "fake://foo.bar/media0/playlist.m3u8"; + // The playlist has no hold back defined. + String playlist = + "#EXTM3U\n" + + "#EXT-X-PROGRAM-DATE-TIME:2020-01-01T00:00:00.0+00:00\n" + + "#EXT-X-TARGETDURATION:4\n" + + "#EXT-X-VERSION:3\n" + + "#EXT-X-MEDIA-SEQUENCE:0\n" + + "#EXTINF:4.00000,\n" + + "fileSequence0.ts"; + // The playlist finishes 1 second before the current time. This should not affect the target + // live offset set in the media item. + SystemClock.setCurrentTimeMillis(Util.parseXsDateTime("2020-01-01T00:00:05.0+00:00")); + HlsMediaSource.Factory factory = createHlsMediaSourceFactory(playlistUri, playlist); + MediaItem mediaItem = + new MediaItem.Builder() + .setUri(playlistUri) + .setLiveConfiguration( + new MediaItem.LiveConfiguration.Builder() + .setTargetOffsetMs(1000) + .setMinPlaybackSpeed(0.94f) + .setMaxPlaybackSpeed(1.02f) + .build()) + .build(); + HlsMediaSource mediaSource = factory.createMediaSource(mediaItem); + + Timeline timeline = prepareAndWaitForTimeline(mediaSource); + + Timeline.Window window = timeline.getWindow(0, new Timeline.Window()); + assertThat(window.liveConfiguration).isEqualTo(mediaItem.liveConfiguration); + assertThat(window.defaultPositionUs).isEqualTo(0); + } + @Test public void loadLivePlaylist_targetLiveOffsetLargerThanLiveWindow_targetLiveOffsetIsWithinLiveWindow()