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,