From 021291b38ff81d0e5fdee13e82819d633a9f9209 Mon Sep 17 00:00:00 2001 From: tonihei Date: Tue, 17 Dec 2019 14:51:06 +0000 Subject: [PATCH] Add server-client time offset to Window. This offset allows to improve the calculated live offset because it can take known client-server time offsets into account. PiperOrigin-RevId: 285970738 --- .../exoplayer2/ext/cast/CastTimeline.java | 1 + .../google/android/exoplayer2/BasePlayer.java | 2 +- .../google/android/exoplayer2/Timeline.java | 34 ++++++++++++++++--- .../exoplayer2/source/MaskingMediaSource.java | 1 + .../source/SinglePeriodTimeline.java | 17 +++++++--- .../google/android/exoplayer2/util/Util.java | 14 ++++++++ .../android/exoplayer2/TimelineTest.java | 1 + .../source/dash/DashChunkSource.java | 3 +- .../source/dash/DashMediaSource.java | 19 +++++------ .../source/dash/DefaultDashChunkSource.java | 12 ++----- .../exoplayer2/source/hls/HlsMediaSource.java | 2 ++ .../exoplayer2/testutil/FakeTimeline.java | 1 + 12 files changed, 77 insertions(+), 30 deletions(-) diff --git a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastTimeline.java b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastTimeline.java index a3bdc5e415..38a7a692b2 100644 --- a/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastTimeline.java +++ b/extensions/cast/src/main/java/com/google/android/exoplayer2/ext/cast/CastTimeline.java @@ -130,6 +130,7 @@ import java.util.Arrays; /* manifest= */ null, /* presentationStartTimeMs= */ C.TIME_UNSET, /* windowStartTimeMs= */ C.TIME_UNSET, + /* elapsedRealtimeEpochOffsetMs= */ C.TIME_UNSET, /* isSeekable= */ !isDynamic, isDynamic, isLive[windowIndex], diff --git a/library/core/src/main/java/com/google/android/exoplayer2/BasePlayer.java b/library/core/src/main/java/com/google/android/exoplayer2/BasePlayer.java index 42cf11cdae..0f00621676 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/BasePlayer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/BasePlayer.java @@ -153,7 +153,7 @@ public abstract class BasePlayer implements Player { if (windowStartTimeMs == C.TIME_UNSET) { return C.TIME_UNSET; } - return System.currentTimeMillis() - window.windowStartTimeMs - getContentPosition(); + return window.getCurrentUnixTimeMs() - window.windowStartTimeMs - getContentPosition(); } @Override diff --git a/library/core/src/main/java/com/google/android/exoplayer2/Timeline.java b/library/core/src/main/java/com/google/android/exoplayer2/Timeline.java index 93a87da0dc..b07a9792a1 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/Timeline.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/Timeline.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer2; +import android.os.SystemClock; import android.util.Pair; import androidx.annotation.Nullable; import com.google.android.exoplayer2.source.ads.AdPlaybackState; @@ -136,19 +137,28 @@ public abstract class Timeline { /** * The start time of the presentation to which this window belongs in milliseconds since the - * epoch, or {@link C#TIME_UNSET} if unknown or not applicable. For informational purposes only. + * Unix epoch, or {@link C#TIME_UNSET} if unknown or not applicable. For informational purposes + * only. */ public long presentationStartTimeMs; /** - * The window's start time in milliseconds since the epoch, or {@link C#TIME_UNSET} if unknown - * or not applicable. For informational purposes only. + * The window's start time in milliseconds since the Unix epoch, or {@link C#TIME_UNSET} if + * unknown or not applicable. For informational purposes only. */ public long windowStartTimeMs; /** - * Whether it's possible to seek within this window. + * The offset between {@link SystemClock#elapsedRealtime()} and the time since the Unix epoch + * according to the clock of the media origin server, or {@link C#TIME_UNSET} if unknown or not + * applicable. + * + *

Note that the current Unix time can be retrieved using {@link #getCurrentUnixTimeMs()} and + * is calculated as {@code SystemClock.elapsedRealtime() + elapsedRealtimeEpochOffsetMs}. */ + public long elapsedRealtimeEpochOffsetMs; + + /** Whether it's possible to seek within this window. */ public boolean isSeekable; // TODO: Split this to better describe which parts of the window might change. For example it @@ -205,6 +215,7 @@ public abstract class Timeline { @Nullable Object manifest, long presentationStartTimeMs, long windowStartTimeMs, + long elapsedRealtimeEpochOffsetMs, boolean isSeekable, boolean isDynamic, boolean isLive, @@ -218,6 +229,7 @@ public abstract class Timeline { this.manifest = manifest; this.presentationStartTimeMs = presentationStartTimeMs; this.windowStartTimeMs = windowStartTimeMs; + this.elapsedRealtimeEpochOffsetMs = elapsedRealtimeEpochOffsetMs; this.isSeekable = isSeekable; this.isDynamic = isDynamic; this.isLive = isLive; @@ -279,6 +291,16 @@ public abstract class Timeline { return positionInFirstPeriodUs; } + /** + * Returns the current time in milliseconds since the Unix epoch. + * + *

This method applies {@link #elapsedRealtimeEpochOffsetMs known corrections} made available + * by the media such that this time corresponds to the clock of the media origin server. + */ + public long getCurrentUnixTimeMs() { + return Util.getNowUnixTimeMs(elapsedRealtimeEpochOffsetMs); + } + @Override public boolean equals(@Nullable Object obj) { if (this == obj) { @@ -293,6 +315,7 @@ public abstract class Timeline { && Util.areEqual(manifest, that.manifest) && presentationStartTimeMs == that.presentationStartTimeMs && windowStartTimeMs == that.windowStartTimeMs + && elapsedRealtimeEpochOffsetMs == that.elapsedRealtimeEpochOffsetMs && isSeekable == that.isSeekable && isDynamic == that.isDynamic && isLive == that.isLive @@ -311,6 +334,9 @@ public abstract class Timeline { result = 31 * result + (manifest == null ? 0 : manifest.hashCode()); result = 31 * result + (int) (presentationStartTimeMs ^ (presentationStartTimeMs >>> 32)); result = 31 * result + (int) (windowStartTimeMs ^ (windowStartTimeMs >>> 32)); + result = + 31 * result + + (int) (elapsedRealtimeEpochOffsetMs ^ (elapsedRealtimeEpochOffsetMs >>> 32)); result = 31 * result + (isSeekable ? 1 : 0); result = 31 * result + (isDynamic ? 1 : 0); result = 31 * result + (isLive ? 1 : 0); diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/MaskingMediaSource.java b/library/core/src/main/java/com/google/android/exoplayer2/source/MaskingMediaSource.java index 213a8b0272..aff40d891d 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/MaskingMediaSource.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/MaskingMediaSource.java @@ -337,6 +337,7 @@ public final class MaskingMediaSource extends CompositeMediaSource { /* manifest= */ null, /* presentationStartTimeMs= */ C.TIME_UNSET, /* windowStartTimeMs= */ C.TIME_UNSET, + /* elapsedRealtimeEpochOffsetMs= */ C.TIME_UNSET, /* isSeekable= */ false, // Dynamic window to indicate pending timeline updates. /* isDynamic= */ true, diff --git a/library/core/src/main/java/com/google/android/exoplayer2/source/SinglePeriodTimeline.java b/library/core/src/main/java/com/google/android/exoplayer2/source/SinglePeriodTimeline.java index 45f64cacf2..5b47398dd5 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/source/SinglePeriodTimeline.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/source/SinglePeriodTimeline.java @@ -29,6 +29,7 @@ public final class SinglePeriodTimeline extends Timeline { private final long presentationStartTimeMs; private final long windowStartTimeMs; + private final long elapsedRealtimeEpochOffsetMs; private final long periodDurationUs; private final long windowDurationUs; private final long windowPositionInPeriodUs; @@ -110,6 +111,7 @@ public final class SinglePeriodTimeline extends Timeline { this( /* presentationStartTimeMs= */ C.TIME_UNSET, /* windowStartTimeMs= */ C.TIME_UNSET, + /* elapsedRealtimeEpochOffsetMs= */ C.TIME_UNSET, periodDurationUs, windowDurationUs, windowPositionInPeriodUs, @@ -126,8 +128,12 @@ public final class SinglePeriodTimeline extends Timeline { * position in the period. * * @param presentationStartTimeMs The start time of the presentation in milliseconds since the - * epoch. - * @param windowStartTimeMs The window's start time in milliseconds since the epoch. + * epoch, or {@link C#TIME_UNSET} if unknown or not applicable. + * @param windowStartTimeMs The window's start time in milliseconds since the epoch, or {@link + * C#TIME_UNSET} if unknown or not applicable. + * @param elapsedRealtimeEpochOffsetMs The offset between {@link + * android.os.SystemClock#elapsedRealtime()} and the time since the Unix epoch according to + * the clock of the media origin server, or {@link C#TIME_UNSET} if unknown or not applicable. * @param periodDurationUs The duration of the period in microseconds. * @param windowDurationUs The duration of the window in microseconds. * @param windowPositionInPeriodUs The position of the start of the window in the period, in @@ -143,6 +149,7 @@ public final class SinglePeriodTimeline extends Timeline { public SinglePeriodTimeline( long presentationStartTimeMs, long windowStartTimeMs, + long elapsedRealtimeEpochOffsetMs, long periodDurationUs, long windowDurationUs, long windowPositionInPeriodUs, @@ -154,6 +161,7 @@ public final class SinglePeriodTimeline extends Timeline { @Nullable Object tag) { this.presentationStartTimeMs = presentationStartTimeMs; this.windowStartTimeMs = windowStartTimeMs; + this.elapsedRealtimeEpochOffsetMs = elapsedRealtimeEpochOffsetMs; this.periodDurationUs = periodDurationUs; this.windowDurationUs = windowDurationUs; this.windowPositionInPeriodUs = windowPositionInPeriodUs; @@ -192,13 +200,14 @@ public final class SinglePeriodTimeline extends Timeline { manifest, presentationStartTimeMs, windowStartTimeMs, + elapsedRealtimeEpochOffsetMs, isSeekable, isDynamic, isLive, windowDefaultStartPositionUs, windowDurationUs, - 0, - 0, + /* firstPeriodIndex= */ 0, + /* lastPeriodIndex= */ 0, windowPositionInPeriodUs); } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/util/Util.java b/library/core/src/main/java/com/google/android/exoplayer2/util/Util.java index 5d20de1bcf..54e65797f0 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/util/Util.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/util/Util.java @@ -39,6 +39,7 @@ import android.os.Build; import android.os.Handler; import android.os.Looper; import android.os.Parcel; +import android.os.SystemClock; import android.security.NetworkSecurityPolicy; import android.telephony.TelephonyManager; import android.text.TextUtils; @@ -2045,6 +2046,19 @@ public final class Util { } } + /** + * Returns the current time in milliseconds since the epoch. + * + * @param elapsedRealtimeEpochOffsetMs The offset between {@link SystemClock#elapsedRealtime()} + * and the time since the Unix epoch, or {@link C#TIME_UNSET} if unknown. + * @return The Unix time in milliseconds since the epoch. + */ + public static long getNowUnixTimeMs(long elapsedRealtimeEpochOffsetMs) { + return elapsedRealtimeEpochOffsetMs == C.TIME_UNSET + ? System.currentTimeMillis() + : SystemClock.elapsedRealtime() + elapsedRealtimeEpochOffsetMs; + } + @Nullable private static String getSystemProperty(String name) { try { diff --git a/library/core/src/test/java/com/google/android/exoplayer2/TimelineTest.java b/library/core/src/test/java/com/google/android/exoplayer2/TimelineTest.java index 5110ad411c..6bc70e3188 100644 --- a/library/core/src/test/java/com/google/android/exoplayer2/TimelineTest.java +++ b/library/core/src/test/java/com/google/android/exoplayer2/TimelineTest.java @@ -134,6 +134,7 @@ public class TimelineTest { window.manifest, window.presentationStartTimeMs, window.windowStartTimeMs, + window.elapsedRealtimeEpochOffsetMs, window.isSeekable, window.isDynamic, window.isLive, diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashChunkSource.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashChunkSource.java index f7edf62182..e12a67a754 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashChunkSource.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DashChunkSource.java @@ -42,7 +42,8 @@ public interface DashChunkSource extends ChunkSource { * @param trackSelection The track selection. * @param elapsedRealtimeOffsetMs If known, an estimate of the instantaneous difference between * server-side unix time and {@link SystemClock#elapsedRealtime()} in milliseconds, - * specified as the server's unix time minus the local elapsed time. If unknown, set to 0. + * specified as the server's unix time minus the local elapsed time. Or {@link + * com.google.android.exoplayer2.C#TIME_UNSET} if unknown. * @param enableEventMessageTrack Whether to output an event message track. * @param closedCaptionFormats The {@link Format Formats} of closed caption tracks to be output. * @param transferListener The transfer listener which should be informed of any data transfers. 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 f919e8eade..bced8b3126 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 @@ -621,6 +621,7 @@ public final class DashMediaSource extends BaseMediaSource { periodsById = new SparseArray<>(); playerEmsgCallback = new DefaultPlayerEmsgCallback(); expiredManifestPublishTimeUs = C.TIME_UNSET; + elapsedRealtimeOffsetMs = C.TIME_UNSET; if (sideloadedManifest) { Assertions.checkState(!manifest.dynamic); manifestCallback = null; @@ -723,7 +724,7 @@ public final class DashMediaSource extends BaseMediaSource { handler.removeCallbacksAndMessages(null); handler = null; } - elapsedRealtimeOffsetMs = 0; + elapsedRealtimeOffsetMs = C.TIME_UNSET; staleManifestReloadAttempt = 0; expiredManifestPublishTimeUs = C.TIME_UNSET; firstPeriodId = 0; @@ -969,7 +970,8 @@ public final class DashMediaSource extends BaseMediaSource { if (manifest.dynamic && !lastPeriodSeekInfo.isIndexExplicit) { // The manifest describes an incomplete live stream. Update the start/end times to reflect the // live stream duration and the manifest's time shift buffer depth. - long liveStreamDurationUs = getNowUnixTimeUs() - C.msToUs(manifest.availabilityStartTimeMs); + long nowUnixTimeUs = C.msToUs(Util.getNowUnixTimeMs(elapsedRealtimeOffsetMs)); + long liveStreamDurationUs = nowUnixTimeUs - C.msToUs(manifest.availabilityStartTimeMs); long liveStreamEndPositionInLastPeriodUs = liveStreamDurationUs - C.msToUs(manifest.getPeriod(lastPeriodIndex).startMs); currentEndTimeUs = Math.min(liveStreamEndPositionInLastPeriodUs, currentEndTimeUs); @@ -1022,6 +1024,7 @@ public final class DashMediaSource extends BaseMediaSource { new DashTimeline( manifest.availabilityStartTimeMs, windowStartTimeMs, + elapsedRealtimeOffsetMs, firstPeriodId, currentStartTimeUs, windowDurationUs, @@ -1093,14 +1096,6 @@ public final class DashMediaSource extends BaseMediaSource { manifestEventDispatcher.loadStarted(loadable.dataSpec, loadable.type, elapsedRealtimeMs); } - private long getNowUnixTimeUs() { - if (elapsedRealtimeOffsetMs != 0) { - return C.msToUs(SystemClock.elapsedRealtime() + elapsedRealtimeOffsetMs); - } else { - return C.msToUs(System.currentTimeMillis()); - } - } - private static final class PeriodSeekInfo { public static PeriodSeekInfo createPeriodSeekInfo( @@ -1170,6 +1165,7 @@ public final class DashMediaSource extends BaseMediaSource { private final long presentationStartTimeMs; private final long windowStartTimeMs; + private final long elapsedRealtimeEpochOffsetMs; private final int firstPeriodId; private final long offsetInFirstPeriodUs; @@ -1181,6 +1177,7 @@ public final class DashMediaSource extends BaseMediaSource { public DashTimeline( long presentationStartTimeMs, long windowStartTimeMs, + long elapsedRealtimeEpochOffsetMs, int firstPeriodId, long offsetInFirstPeriodUs, long windowDurationUs, @@ -1189,6 +1186,7 @@ public final class DashMediaSource extends BaseMediaSource { @Nullable Object windowTag) { this.presentationStartTimeMs = presentationStartTimeMs; this.windowStartTimeMs = windowStartTimeMs; + this.elapsedRealtimeEpochOffsetMs = elapsedRealtimeEpochOffsetMs; this.firstPeriodId = firstPeriodId; this.offsetInFirstPeriodUs = offsetInFirstPeriodUs; this.windowDurationUs = windowDurationUs; @@ -1228,6 +1226,7 @@ public final class DashMediaSource extends BaseMediaSource { manifest, presentationStartTimeMs, windowStartTimeMs, + elapsedRealtimeEpochOffsetMs, /* isSeekable= */ true, /* isDynamic= */ isMovingLiveWindow(manifest), /* isLive= */ manifest.dynamic, diff --git a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DefaultDashChunkSource.java b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DefaultDashChunkSource.java index 2904944493..340c5b9b64 100644 --- a/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DefaultDashChunkSource.java +++ b/library/dash/src/main/java/com/google/android/exoplayer2/source/dash/DefaultDashChunkSource.java @@ -136,7 +136,7 @@ public class DefaultDashChunkSource implements DashChunkSource { * @param dataSource A {@link DataSource} suitable for loading the media data. * @param elapsedRealtimeOffsetMs If known, an estimate of the instantaneous difference between * server-side unix time and {@link SystemClock#elapsedRealtime()} in milliseconds, specified - * as the server's unix time minus the local elapsed time. If unknown, set to 0. + * as the server's unix time minus the local elapsed time. Or {@link C#TIME_UNSET} if unknown. * @param maxSegmentsPerLoad The maximum number of segments to combine into a single request. Note * that segments will only be combined if their {@link Uri}s are the same and if their data * ranges are adjacent. @@ -267,7 +267,7 @@ public class DefaultDashChunkSource implements DashChunkSource { return; } - long nowUnixTimeUs = getNowUnixTimeUs(); + long nowUnixTimeUs = C.msToUs(Util.getNowUnixTimeMs(elapsedRealtimeOffsetMs)); MediaChunk previous = queue.isEmpty() ? null : queue.get(queue.size() - 1); MediaChunkIterator[] chunkIterators = new MediaChunkIterator[trackSelection.length()]; for (int i = 0; i < chunkIterators.length; i++) { @@ -474,14 +474,6 @@ public class DefaultDashChunkSource implements DashChunkSource { ? representationHolder.getSegmentEndTimeUs(lastAvailableSegmentNum) : C.TIME_UNSET; } - private long getNowUnixTimeUs() { - if (elapsedRealtimeOffsetMs != 0) { - return (SystemClock.elapsedRealtime() + elapsedRealtimeOffsetMs) * 1000; - } else { - return System.currentTimeMillis() * 1000; - } - } - private long resolveTimeToLiveEdgeUs(long playbackPositionUs) { boolean resolveTimeToLiveEdgePossible = manifest.dynamic && liveEdgeTimeUs != C.TIME_UNSET; return resolveTimeToLiveEdgePossible ? liveEdgeTimeUs - playbackPositionUs : C.TIME_UNSET; 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 8798d08039..411eb448be 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 @@ -468,6 +468,7 @@ public final class HlsMediaSource extends BaseMediaSource new SinglePeriodTimeline( presentationStartTimeMs, windowStartTimeMs, + /* elapsedRealtimeEpochOffsetMs= */ C.TIME_UNSET, periodDurationUs, /* windowDurationUs= */ playlist.durationUs, /* windowPositionInPeriodUs= */ offsetFromInitialStartTimeUs, @@ -485,6 +486,7 @@ public final class HlsMediaSource extends BaseMediaSource new SinglePeriodTimeline( presentationStartTimeMs, windowStartTimeMs, + /* elapsedRealtimeEpochOffsetMs= */ C.TIME_UNSET, /* periodDurationUs= */ playlist.durationUs, /* windowDurationUs= */ playlist.durationUs, /* windowPositionInPeriodUs= */ 0, diff --git a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTimeline.java b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTimeline.java index c8c7190007..8160dc3147 100644 --- a/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTimeline.java +++ b/testutils/src/main/java/com/google/android/exoplayer2/testutil/FakeTimeline.java @@ -219,6 +219,7 @@ public final class FakeTimeline extends Timeline { manifests[windowIndex], /* presentationStartTimeMs= */ C.TIME_UNSET, /* windowStartTimeMs= */ C.TIME_UNSET, + /* elapsedRealtimeEpochOffsetMs= */ C.TIME_UNSET, windowDefinition.isSeekable, windowDefinition.isDynamic, windowDefinition.isLive,