mirror of
https://github.com/samsonjs/media.git
synced 2026-04-08 11:45:51 +00:00
Support dynamic TimeRange for DASH live.
This commit is contained in:
parent
d5f8d1a1b1
commit
f8824ac390
4 changed files with 193 additions and 109 deletions
|
|
@ -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 {
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
Loading…
Reference in a new issue