From f8824ac390fc070bfd23cc54f51ce17b4de5d4d7 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Tue, 1 Sep 2015 14:14:10 +0100 Subject: [PATCH] Support dynamic TimeRange for DASH live. --- .../android/exoplayer/demo/EventLogger.java | 4 +- .../android/exoplayer/TimeRangeTest.java | 10 +- .../google/android/exoplayer/TimeRange.java | 207 +++++++++++++----- .../exoplayer/dash/DashChunkSource.java | 81 +++---- 4 files changed, 193 insertions(+), 109 deletions(-) diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/EventLogger.java b/demo/src/main/java/com/google/android/exoplayer/demo/EventLogger.java index 8d8ea7e55c..f5b698441c 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/EventLogger.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/EventLogger.java @@ -173,8 +173,8 @@ public class EventLogger implements DemoPlayer.Listener, DemoPlayer.InfoListener @Override public void onAvailableRangeChanged(TimeRange availableRange) { availableRangeValuesUs = availableRange.getCurrentBoundsUs(availableRangeValuesUs); - Log.d(TAG, "availableRange [ " + availableRange.type + ", " + availableRangeValuesUs[0] + ", " - + availableRangeValuesUs[1] + "]"); + Log.d(TAG, "availableRange [" + availableRange.isStatic() + ", " + availableRangeValuesUs[0] + + ", " + availableRangeValuesUs[1] + "]"); } private void printInternalError(String type, Exception e) { diff --git a/library/src/androidTest/java/com/google/android/exoplayer/TimeRangeTest.java b/library/src/androidTest/java/com/google/android/exoplayer/TimeRangeTest.java index 58df649d65..dee65dd768 100644 --- a/library/src/androidTest/java/com/google/android/exoplayer/TimeRangeTest.java +++ b/library/src/androidTest/java/com/google/android/exoplayer/TimeRangeTest.java @@ -15,6 +15,8 @@ */ package com.google.android.exoplayer; +import com.google.android.exoplayer.TimeRange.StaticTimeRange; + import junit.framework.TestCase; /** @@ -22,14 +24,14 @@ import junit.framework.TestCase; */ public class TimeRangeTest extends TestCase { - public void testEquals() { - TimeRange timeRange1 = new TimeRange(TimeRange.TYPE_SNAPSHOT, 0, 30000000); + public void testStaticEquals() { + TimeRange timeRange1 = new StaticTimeRange(0, 30000000); assertTrue(timeRange1.equals(timeRange1)); - TimeRange timeRange2 = new TimeRange(TimeRange.TYPE_SNAPSHOT, 0, 30000000); + TimeRange timeRange2 = new StaticTimeRange(0, 30000000); assertTrue(timeRange1.equals(timeRange2)); - TimeRange timeRange3 = new TimeRange(TimeRange.TYPE_SNAPSHOT, 0, 60000000); + TimeRange timeRange3 = new StaticTimeRange(0, 60000000); assertFalse(timeRange1.equals(timeRange3)); } diff --git a/library/src/main/java/com/google/android/exoplayer/TimeRange.java b/library/src/main/java/com/google/android/exoplayer/TimeRange.java index 03bc528749..46db5aa059 100644 --- a/library/src/main/java/com/google/android/exoplayer/TimeRange.java +++ b/library/src/main/java/com/google/android/exoplayer/TimeRange.java @@ -15,37 +15,22 @@ */ package com.google.android.exoplayer; +import com.google.android.exoplayer.util.Clock; + +import android.os.SystemClock; + /** * A container to store a start and end time in microseconds. */ -public final class TimeRange { +public interface TimeRange { /** - * Represents a range of time whose bounds change in bulk increments rather than smoothly over - * time. - */ - public static final int TYPE_SNAPSHOT = 0; - - /** - * The type of this time range. - */ - public final int type; - - private final long startTimeUs; - private final long endTimeUs; - - /** - * Create a new {@link TimeRange} of the appropriate type. + * Whether the range is static, meaning repeated calls to {@link #getCurrentBoundsMs(long[])} + * or {@link #getCurrentBoundsUs(long[])} will return identical results. * - * @param type The type of the TimeRange. - * @param startTimeUs The beginning of the TimeRange. - * @param endTimeUs The end of the TimeRange. + * @return Whether the range is static. */ - public TimeRange(int type, long startTimeUs, long endTimeUs) { - this.type = type; - this.startTimeUs = startTimeUs; - this.endTimeUs = endTimeUs; - } + public boolean isStatic(); /** * Returns the start and end times (in milliseconds) of the TimeRange in the provided array, @@ -54,12 +39,7 @@ public final class TimeRange { * @param out An array to store the start and end times; can be null. * @return An array containing the start time (index 0) and end time (index 1) in milliseconds. */ - public long[] getCurrentBoundsMs(long[] out) { - out = getCurrentBoundsUs(out); - out[0] /= 1000; - out[1] /= 1000; - return out; - } + public long[] getCurrentBoundsMs(long[] out); /** * Returns the start and end times (in microseconds) of the TimeRange in the provided array, @@ -68,35 +48,156 @@ public final class TimeRange { * @param out An array to store the start and end times; can be null. * @return An array containing the start time (index 0) and end time (index 1) in microseconds. */ - public long[] getCurrentBoundsUs(long[] out) { - if (out == null || out.length < 2) { - out = new long[2]; + public long[] getCurrentBoundsUs(long[] out); + + /** + * A static {@link TimeRange}. + */ + public static final class StaticTimeRange implements TimeRange { + + private final long startTimeUs; + private final long endTimeUs; + + /** + * @param startTimeUs The beginning of the range. + * @param endTimeUs The end of the range. + */ + public StaticTimeRange(long startTimeUs, long endTimeUs) { + this.startTimeUs = startTimeUs; + this.endTimeUs = endTimeUs; } - out[0] = startTimeUs; - out[1] = endTimeUs; - return out; - } - @Override - public int hashCode() { - int hashCode = 0; - hashCode |= type << 30; - hashCode |= (((startTimeUs + endTimeUs) / 1000) & 0x3FFFFFFF); - return hashCode; - } - - @Override - public boolean equals(Object other) { - if (other == this) { + @Override + public boolean isStatic() { return true; } - if (other instanceof TimeRange) { - TimeRange otherTimeRange = (TimeRange) other; - return (otherTimeRange.type == type) && (otherTimeRange.startTimeUs == startTimeUs) - && (otherTimeRange.endTimeUs == endTimeUs); - } else { + + @Override + public long[] getCurrentBoundsMs(long[] out) { + out = getCurrentBoundsUs(out); + out[0] /= 1000; + out[1] /= 1000; + return out; + } + + @Override + public long[] getCurrentBoundsUs(long[] out) { + if (out == null || out.length < 2) { + out = new long[2]; + } + out[0] = startTimeUs; + out[1] = endTimeUs; + return out; + } + + @Override + public int hashCode() { + int result = 17; + result = 31 * result + (int) startTimeUs; + result = 31 * result + (int) endTimeUs; + return result; + } + + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + StaticTimeRange other = (StaticTimeRange) obj; + return other.startTimeUs == startTimeUs + && other.endTimeUs == endTimeUs; + } + + } + + /** + * A dynamic {@link TimeRange}. + */ + public static final class DynamicTimeRange implements TimeRange { + + private final long minStartTimeUs; + private final long maxEndTimeUs; + private final long elapsedRealtimeAtStartUs; + private final long bufferDepthUs; + private final Clock systemClock; + + /** + * @param minStartTimeUs A lower bound on the beginning of the range. + * @param maxEndTimeUs An upper bound on the end of the range. + * @param elapsedRealtimeAtStartUs The value of {@link SystemClock#elapsedRealtime()}, + * multiplied by 1000, corresponding to a media time of zero. + * @param bufferDepthUs The buffer depth of the media, or -1. + * @param systemClock A system clock. + */ + public DynamicTimeRange(long minStartTimeUs, long maxEndTimeUs, long elapsedRealtimeAtStartUs, + long bufferDepthUs, Clock systemClock) { + this.minStartTimeUs = minStartTimeUs; + this.maxEndTimeUs = maxEndTimeUs; + this.elapsedRealtimeAtStartUs = elapsedRealtimeAtStartUs; + this.bufferDepthUs = bufferDepthUs; + this.systemClock = systemClock; + } + + @Override + public boolean isStatic() { return false; } + + @Override + public long[] getCurrentBoundsMs(long[] out) { + out = getCurrentBoundsUs(out); + out[0] /= 1000; + out[1] /= 1000; + return out; + } + + @Override + public long[] getCurrentBoundsUs(long[] out) { + if (out == null || out.length < 2) { + out = new long[2]; + } + // Don't allow the end time to be greater than the total elapsed time. + long currentEndTimeUs = Math.min(maxEndTimeUs, + (systemClock.elapsedRealtime() * 1000) - elapsedRealtimeAtStartUs); + long currentStartTimeUs = minStartTimeUs; + if (bufferDepthUs != -1) { + // Don't allow the start time to be less than the current end time minus the buffer depth. + currentStartTimeUs = Math.max(currentStartTimeUs, + currentEndTimeUs - bufferDepthUs); + } + out[0] = currentStartTimeUs; + out[1] = currentEndTimeUs; + return out; + } + + @Override + public int hashCode() { + int result = 17; + result = 31 * result + (int) minStartTimeUs; + result = 31 * result + (int) maxEndTimeUs; + result = 31 * result + (int) elapsedRealtimeAtStartUs; + result = 31 * result + (int) bufferDepthUs; + return result; + } + + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + DynamicTimeRange other = (DynamicTimeRange) obj; + return other.minStartTimeUs == minStartTimeUs + && other.maxEndTimeUs == maxEndTimeUs + && other.elapsedRealtimeAtStartUs == elapsedRealtimeAtStartUs + && other.bufferDepthUs == bufferDepthUs; + } + } } diff --git a/library/src/main/java/com/google/android/exoplayer/dash/DashChunkSource.java b/library/src/main/java/com/google/android/exoplayer/dash/DashChunkSource.java index 61965c32fc..8f56f68307 100644 --- a/library/src/main/java/com/google/android/exoplayer/dash/DashChunkSource.java +++ b/library/src/main/java/com/google/android/exoplayer/dash/DashChunkSource.java @@ -18,6 +18,8 @@ package com.google.android.exoplayer.dash; import com.google.android.exoplayer.BehindLiveWindowException; import com.google.android.exoplayer.MediaFormat; import com.google.android.exoplayer.TimeRange; +import com.google.android.exoplayer.TimeRange.DynamicTimeRange; +import com.google.android.exoplayer.TimeRange.StaticTimeRange; import com.google.android.exoplayer.TrackRenderer; import com.google.android.exoplayer.chunk.Chunk; import com.google.android.exoplayer.chunk.ChunkExtractorWrapper; @@ -115,6 +117,7 @@ public class DashChunkSource implements ChunkSource { private final long elapsedRealtimeOffsetUs; private final int maxWidth; private final int maxHeight; + private final long[] availableRangeValues; private final SparseArray periodHolders; @@ -128,7 +131,6 @@ public class DashChunkSource implements ChunkSource { private DrmInitData drmInitData; private TimeRange availableRange; - private long[] availableRangeValues; private boolean startAtLiveEdge; private boolean lastChunkWasInitialization; @@ -327,7 +329,6 @@ public class DashChunkSource implements ChunkSource { if (manifestFetcher != null) { manifestFetcher.enable(); } - updateAvailableBounds(getNowUs()); } @Override @@ -348,7 +349,6 @@ public class DashChunkSource implements ChunkSource { MediaPresentationDescription newManifest = manifestFetcher.getManifest(); if (currentManifest != newManifest && newManifest != null) { processManifest(newManifest); - updateAvailableBounds(getNowUs()); } // TODO: This is a temporary hack to avoid constantly refreshing the MPD in cases where @@ -401,19 +401,12 @@ public class DashChunkSource implements ChunkSource { // In all cases where we return before instantiating a new chunk, we want out.chunk to be null. out.chunk = null; - if (currentManifest.dynamic - && periodHolders.valueAt(periodHolders.size() - 1).isIndexUnbounded()) { - // Manifests with unbounded indexes aren't updated regularly, so we need to update the - // segment bounds before use to ensure that they are accurate to the current time - updateAvailableBounds(getNowUs()); - } - availableRangeValues = availableRange.getCurrentBoundsUs(availableRangeValues); - long segmentStartTimeUs; int segmentNum = -1; boolean startingNewPeriod = false; PeriodHolder periodHolder; + availableRange.getCurrentBoundsUs(availableRangeValues); if (queue.isEmpty()) { if (currentManifest.dynamic) { if (startAtLiveEdge) { @@ -565,45 +558,6 @@ public class DashChunkSource implements ChunkSource { // Do nothing. } - private void updateAvailableBounds(long nowUs) { - PeriodHolder firstPeriod = periodHolders.valueAt(0); - long earliestAvailablePosition = firstPeriod.getAvailableStartTimeUs(); - PeriodHolder lastPeriod = periodHolders.valueAt(periodHolders.size() - 1); - boolean isManifestUnbounded = lastPeriod.isIndexUnbounded(); - long latestAvailablePosition; - if (!currentManifest.dynamic || !isManifestUnbounded) { - latestAvailablePosition = lastPeriod.getAvailableEndTimeUs(); - } else { - latestAvailablePosition = TrackRenderer.UNKNOWN_TIME_US; - } - - if (currentManifest.dynamic) { - if (isManifestUnbounded) { - latestAvailablePosition = nowUs - currentManifest.availabilityStartTime * 1000; - } else if (!lastPeriod.isIndexExplicit()) { - // Some segments defined by the index may not be available yet. Bound the calculated live - // edge based on the elapsed time since the manifest became available. - latestAvailablePosition = Math.min(latestAvailablePosition, - nowUs - currentManifest.availabilityStartTime * 1000); - } - - // if we have a limited timeshift buffer, we need to adjust the earliest seek position so - // that it doesn't start before the buffer - if (currentManifest.timeShiftBufferDepth != -1) { - long bufferDepthUs = currentManifest.timeShiftBufferDepth * 1000; - earliestAvailablePosition = Math.max(earliestAvailablePosition, - latestAvailablePosition - bufferDepthUs); - } - } - - TimeRange newAvailableRange = new TimeRange(TimeRange.TYPE_SNAPSHOT, earliestAvailablePosition, - latestAvailablePosition); - if (availableRange == null || !availableRange.equals(newAvailableRange)) { - availableRange = newAvailableRange; - notifyAvailableRangeChanged(availableRange); - } - } - private static boolean mimeTypeIsWebm(String mimeType) { return mimeType.startsWith(MimeTypes.VIDEO_WEBM) || mimeType.startsWith(MimeTypes.AUDIO_WEBM); } @@ -755,9 +709,36 @@ public class DashChunkSource implements ChunkSource { periodHolderNextIndex++; } + // Update the available range. + TimeRange newAvailableRange = getAvailableRange(getNowUs()); + if (availableRange == null || !availableRange.equals(newAvailableRange)) { + availableRange = newAvailableRange; + notifyAvailableRangeChanged(availableRange); + } + currentManifest = manifest; } + private TimeRange getAvailableRange(long nowUs) { + PeriodHolder firstPeriod = periodHolders.valueAt(0); + PeriodHolder lastPeriod = periodHolders.valueAt(periodHolders.size() - 1); + + if (!currentManifest.dynamic || lastPeriod.isIndexExplicit()) { + return new StaticTimeRange(firstPeriod.getAvailableStartTimeUs(), + lastPeriod.getAvailableEndTimeUs()); + } + + long minStartPositionUs = firstPeriod.getAvailableStartTimeUs(); + long maxEndPositionUs = lastPeriod.isIndexUnbounded() ? Long.MAX_VALUE + : lastPeriod.getAvailableEndTimeUs(); + long elapsedRealtimeAtZeroUs = (systemClock.elapsedRealtime() * 1000) + - (nowUs - currentManifest.availabilityStartTime * 1000); + long timeShiftBufferDepthUs = currentManifest.timeShiftBufferDepth == -1 ? -1 + : currentManifest.timeShiftBufferDepth * 1000; + return new DynamicTimeRange(minStartPositionUs, maxEndPositionUs, elapsedRealtimeAtZeroUs, + timeShiftBufferDepthUs, systemClock); + } + private void notifyAvailableRangeChanged(final TimeRange seekRange) { if (eventHandler != null && eventListener != null) { eventHandler.post(new Runnable() {