Commit f8824ac3 by Oliver Woodman

Support dynamic TimeRange for DASH live.

parent d5f8d1a1
...@@ -173,8 +173,8 @@ public class EventLogger implements DemoPlayer.Listener, DemoPlayer.InfoListener ...@@ -173,8 +173,8 @@ public class EventLogger implements DemoPlayer.Listener, DemoPlayer.InfoListener
@Override @Override
public void onAvailableRangeChanged(TimeRange availableRange) { public void onAvailableRangeChanged(TimeRange availableRange) {
availableRangeValuesUs = availableRange.getCurrentBoundsUs(availableRangeValuesUs); availableRangeValuesUs = availableRange.getCurrentBoundsUs(availableRangeValuesUs);
Log.d(TAG, "availableRange [ " + availableRange.type + ", " + availableRangeValuesUs[0] + ", " Log.d(TAG, "availableRange [" + availableRange.isStatic() + ", " + availableRangeValuesUs[0]
+ availableRangeValuesUs[1] + "]"); + ", " + availableRangeValuesUs[1] + "]");
} }
private void printInternalError(String type, Exception e) { private void printInternalError(String type, Exception e) {
......
...@@ -15,6 +15,8 @@ ...@@ -15,6 +15,8 @@
*/ */
package com.google.android.exoplayer; package com.google.android.exoplayer;
import com.google.android.exoplayer.TimeRange.StaticTimeRange;
import junit.framework.TestCase; import junit.framework.TestCase;
/** /**
...@@ -22,14 +24,14 @@ import junit.framework.TestCase; ...@@ -22,14 +24,14 @@ import junit.framework.TestCase;
*/ */
public class TimeRangeTest extends TestCase { public class TimeRangeTest extends TestCase {
public void testEquals() { public void testStaticEquals() {
TimeRange timeRange1 = new TimeRange(TimeRange.TYPE_SNAPSHOT, 0, 30000000); TimeRange timeRange1 = new StaticTimeRange(0, 30000000);
assertTrue(timeRange1.equals(timeRange1)); assertTrue(timeRange1.equals(timeRange1));
TimeRange timeRange2 = new TimeRange(TimeRange.TYPE_SNAPSHOT, 0, 30000000); TimeRange timeRange2 = new StaticTimeRange(0, 30000000);
assertTrue(timeRange1.equals(timeRange2)); assertTrue(timeRange1.equals(timeRange2));
TimeRange timeRange3 = new TimeRange(TimeRange.TYPE_SNAPSHOT, 0, 60000000); TimeRange timeRange3 = new StaticTimeRange(0, 60000000);
assertFalse(timeRange1.equals(timeRange3)); assertFalse(timeRange1.equals(timeRange3));
} }
......
...@@ -15,37 +15,22 @@ ...@@ -15,37 +15,22 @@
*/ */
package com.google.android.exoplayer; 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. * 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. * Whether the range is static, meaning repeated calls to {@link #getCurrentBoundsMs(long[])}
*/ * or {@link #getCurrentBoundsUs(long[])} will return identical results.
public final int type;
private final long startTimeUs;
private final long endTimeUs;
/**
* Create a new {@link TimeRange} of the appropriate type.
* *
* @param type The type of the TimeRange. * @return Whether the range is static.
* @param startTimeUs The beginning of the TimeRange.
* @param endTimeUs The end of the TimeRange.
*/ */
public TimeRange(int type, long startTimeUs, long endTimeUs) { public boolean isStatic();
this.type = type;
this.startTimeUs = startTimeUs;
this.endTimeUs = endTimeUs;
}
/** /**
* Returns the start and end times (in milliseconds) of the TimeRange in the provided array, * Returns the start and end times (in milliseconds) of the TimeRange in the provided array,
...@@ -54,12 +39,7 @@ public final class TimeRange { ...@@ -54,12 +39,7 @@ public final class TimeRange {
* @param out An array to store the start and end times; can be null. * @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. * @return An array containing the start time (index 0) and end time (index 1) in milliseconds.
*/ */
public long[] getCurrentBoundsMs(long[] out) { public long[] getCurrentBoundsMs(long[] out);
out = getCurrentBoundsUs(out);
out[0] /= 1000;
out[1] /= 1000;
return out;
}
/** /**
* Returns the start and end times (in microseconds) of the TimeRange in the provided array, * Returns the start and end times (in microseconds) of the TimeRange in the provided array,
...@@ -68,35 +48,156 @@ public final class TimeRange { ...@@ -68,35 +48,156 @@ public final class TimeRange {
* @param out An array to store the start and end times; can be null. * @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. * @return An array containing the start time (index 0) and end time (index 1) in microseconds.
*/ */
public long[] getCurrentBoundsUs(long[] out) { public long[] getCurrentBoundsUs(long[] out);
if (out == null || out.length < 2) {
out = new long[2]; /**
* 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;
}
@Override
public boolean isStatic() {
return true;
}
@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;
} }
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) { * A dynamic {@link TimeRange}.
if (other == this) { */
return true; 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;
} }
if (other instanceof TimeRange) {
TimeRange otherTimeRange = (TimeRange) other; @Override
return (otherTimeRange.type == type) && (otherTimeRange.startTimeUs == startTimeUs) public boolean isStatic() {
&& (otherTimeRange.endTimeUs == endTimeUs);
} else {
return false; 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;
}
} }
} }
...@@ -18,6 +18,8 @@ package com.google.android.exoplayer.dash; ...@@ -18,6 +18,8 @@ package com.google.android.exoplayer.dash;
import com.google.android.exoplayer.BehindLiveWindowException; import com.google.android.exoplayer.BehindLiveWindowException;
import com.google.android.exoplayer.MediaFormat; import com.google.android.exoplayer.MediaFormat;
import com.google.android.exoplayer.TimeRange; 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.TrackRenderer;
import com.google.android.exoplayer.chunk.Chunk; import com.google.android.exoplayer.chunk.Chunk;
import com.google.android.exoplayer.chunk.ChunkExtractorWrapper; import com.google.android.exoplayer.chunk.ChunkExtractorWrapper;
...@@ -115,6 +117,7 @@ public class DashChunkSource implements ChunkSource { ...@@ -115,6 +117,7 @@ public class DashChunkSource implements ChunkSource {
private final long elapsedRealtimeOffsetUs; private final long elapsedRealtimeOffsetUs;
private final int maxWidth; private final int maxWidth;
private final int maxHeight; private final int maxHeight;
private final long[] availableRangeValues;
private final SparseArray<PeriodHolder> periodHolders; private final SparseArray<PeriodHolder> periodHolders;
...@@ -128,7 +131,6 @@ public class DashChunkSource implements ChunkSource { ...@@ -128,7 +131,6 @@ public class DashChunkSource implements ChunkSource {
private DrmInitData drmInitData; private DrmInitData drmInitData;
private TimeRange availableRange; private TimeRange availableRange;
private long[] availableRangeValues;
private boolean startAtLiveEdge; private boolean startAtLiveEdge;
private boolean lastChunkWasInitialization; private boolean lastChunkWasInitialization;
...@@ -327,7 +329,6 @@ public class DashChunkSource implements ChunkSource { ...@@ -327,7 +329,6 @@ public class DashChunkSource implements ChunkSource {
if (manifestFetcher != null) { if (manifestFetcher != null) {
manifestFetcher.enable(); manifestFetcher.enable();
} }
updateAvailableBounds(getNowUs());
} }
@Override @Override
...@@ -348,7 +349,6 @@ public class DashChunkSource implements ChunkSource { ...@@ -348,7 +349,6 @@ public class DashChunkSource implements ChunkSource {
MediaPresentationDescription newManifest = manifestFetcher.getManifest(); MediaPresentationDescription newManifest = manifestFetcher.getManifest();
if (currentManifest != newManifest && newManifest != null) { if (currentManifest != newManifest && newManifest != null) {
processManifest(newManifest); processManifest(newManifest);
updateAvailableBounds(getNowUs());
} }
// TODO: This is a temporary hack to avoid constantly refreshing the MPD in cases where // TODO: This is a temporary hack to avoid constantly refreshing the MPD in cases where
...@@ -401,19 +401,12 @@ public class DashChunkSource implements ChunkSource { ...@@ -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. // In all cases where we return before instantiating a new chunk, we want out.chunk to be null.
out.chunk = 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; long segmentStartTimeUs;
int segmentNum = -1; int segmentNum = -1;
boolean startingNewPeriod = false; boolean startingNewPeriod = false;
PeriodHolder periodHolder; PeriodHolder periodHolder;
availableRange.getCurrentBoundsUs(availableRangeValues);
if (queue.isEmpty()) { if (queue.isEmpty()) {
if (currentManifest.dynamic) { if (currentManifest.dynamic) {
if (startAtLiveEdge) { if (startAtLiveEdge) {
...@@ -565,45 +558,6 @@ public class DashChunkSource implements ChunkSource { ...@@ -565,45 +558,6 @@ public class DashChunkSource implements ChunkSource {
// Do nothing. // 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) { private static boolean mimeTypeIsWebm(String mimeType) {
return mimeType.startsWith(MimeTypes.VIDEO_WEBM) || mimeType.startsWith(MimeTypes.AUDIO_WEBM); return mimeType.startsWith(MimeTypes.VIDEO_WEBM) || mimeType.startsWith(MimeTypes.AUDIO_WEBM);
} }
...@@ -755,9 +709,36 @@ public class DashChunkSource implements ChunkSource { ...@@ -755,9 +709,36 @@ public class DashChunkSource implements ChunkSource {
periodHolderNextIndex++; periodHolderNextIndex++;
} }
// Update the available range.
TimeRange newAvailableRange = getAvailableRange(getNowUs());
if (availableRange == null || !availableRange.equals(newAvailableRange)) {
availableRange = newAvailableRange;
notifyAvailableRangeChanged(availableRange);
}
currentManifest = manifest; 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) { private void notifyAvailableRangeChanged(final TimeRange seekRange) {
if (eventHandler != null && eventListener != null) { if (eventHandler != null && eventListener != null) {
eventHandler.post(new Runnable() { eventHandler.post(new Runnable() {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment