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
@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) {
......
......@@ -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));
}
......
......@@ -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 {
/**
* Represents a range of time whose bounds change in bulk increments rather than smoothly over
* time.
*/
public static final int TYPE_SNAPSHOT = 0;
public interface TimeRange {
/**
* 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;
}
@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) {
if (other == this) {
return true;
/**
* 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;
}
if (other instanceof TimeRange) {
TimeRange otherTimeRange = (TimeRange) other;
return (otherTimeRange.type == type) && (otherTimeRange.startTimeUs == startTimeUs)
&& (otherTimeRange.endTimeUs == endTimeUs);
} else {
@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;
}
}
}
......@@ -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<PeriodHolder> 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() {
......
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