Commit 0efaec59 by Oliver Woodman

Implemented limited support for multi-period DASH manifests.

Limitation: Successive periods must expose the same adaptation
sets and representations.

GitHub Issue: #557
parent f69f9489
...@@ -47,7 +47,7 @@ public class EventLogger implements DemoPlayer.Listener, DemoPlayer.InfoListener ...@@ -47,7 +47,7 @@ public class EventLogger implements DemoPlayer.Listener, DemoPlayer.InfoListener
private long sessionStartTimeMs; private long sessionStartTimeMs;
private long[] loadStartTimeMs; private long[] loadStartTimeMs;
private long[] seekRangeValuesUs; private long[] availableRangeValuesUs;
public EventLogger() { public EventLogger() {
loadStartTimeMs = new long[DemoPlayer.RENDERER_COUNT]; loadStartTimeMs = new long[DemoPlayer.RENDERER_COUNT];
...@@ -171,10 +171,10 @@ public class EventLogger implements DemoPlayer.Listener, DemoPlayer.InfoListener ...@@ -171,10 +171,10 @@ public class EventLogger implements DemoPlayer.Listener, DemoPlayer.InfoListener
} }
@Override @Override
public void onSeekRangeChanged(TimeRange seekRange) { public void onAvailableRangeChanged(TimeRange availableRange) {
seekRangeValuesUs = seekRange.getCurrentBoundsUs(seekRangeValuesUs); availableRangeValuesUs = availableRange.getCurrentBoundsUs(availableRangeValuesUs);
Log.d(TAG, "seekRange [ " + seekRange.type + ", " + seekRangeValuesUs[0] + ", " Log.d(TAG, "availableRange [ " + availableRange.type + ", " + availableRangeValuesUs[0] + ", "
+ seekRangeValuesUs[1] + "]"); + availableRangeValuesUs[1] + "]");
} }
private void printInternalError(String type, Exception e) { private void printInternalError(String type, Exception e) {
......
...@@ -124,7 +124,7 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi ...@@ -124,7 +124,7 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi
int mediaStartTimeMs, int mediaEndTimeMs, long elapsedRealtimeMs, long loadDurationMs); int mediaStartTimeMs, int mediaEndTimeMs, long elapsedRealtimeMs, long loadDurationMs);
void onDecoderInitialized(String decoderName, long elapsedRealtimeMs, void onDecoderInitialized(String decoderName, long elapsedRealtimeMs,
long initializationDurationMs); long initializationDurationMs);
void onSeekRangeChanged(TimeRange seekRange); void onAvailableRangeChanged(TimeRange availableRange);
} }
/** /**
...@@ -509,9 +509,9 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi ...@@ -509,9 +509,9 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi
} }
@Override @Override
public void onSeekRangeChanged(TimeRange seekRange) { public void onAvailableRangeChanged(TimeRange availableRange) {
if (infoListener != null) { if (infoListener != null) {
infoListener.onSeekRangeChanged(seekRange); infoListener.onAvailableRangeChanged(availableRange);
} }
} }
......
...@@ -25,6 +25,7 @@ import com.google.android.exoplayer.chunk.ChunkOperationHolder; ...@@ -25,6 +25,7 @@ import com.google.android.exoplayer.chunk.ChunkOperationHolder;
import com.google.android.exoplayer.chunk.Format; import com.google.android.exoplayer.chunk.Format;
import com.google.android.exoplayer.chunk.FormatEvaluator; import com.google.android.exoplayer.chunk.FormatEvaluator;
import com.google.android.exoplayer.chunk.FormatEvaluator.FixedEvaluator; import com.google.android.exoplayer.chunk.FormatEvaluator.FixedEvaluator;
import com.google.android.exoplayer.chunk.InitializationChunk;
import com.google.android.exoplayer.chunk.MediaChunk; import com.google.android.exoplayer.chunk.MediaChunk;
import com.google.android.exoplayer.dash.mpd.AdaptationSet; import com.google.android.exoplayer.dash.mpd.AdaptationSet;
import com.google.android.exoplayer.dash.mpd.MediaPresentationDescription; import com.google.android.exoplayer.dash.mpd.MediaPresentationDescription;
...@@ -64,6 +65,11 @@ public class DashChunkSourceTest extends InstrumentationTestCase { ...@@ -64,6 +65,11 @@ public class DashChunkSourceTest extends InstrumentationTestCase {
private static final long LIVE_DURATION_MS = LIVE_SEGMENT_COUNT * LIVE_SEGMENT_DURATION_MS; private static final long LIVE_DURATION_MS = LIVE_SEGMENT_COUNT * LIVE_SEGMENT_DURATION_MS;
private static final long LIVE_TIMESHIFT_BUFFER_DEPTH_MS = LIVE_DURATION_MS; private static final long LIVE_TIMESHIFT_BUFFER_DEPTH_MS = LIVE_DURATION_MS;
private static final int MULTI_PERIOD_COUNT = 2;
private static final long MULTI_PERIOD_VOD_DURATION_MS = VOD_DURATION_MS * MULTI_PERIOD_COUNT;
private static final long MULTI_PERIOD_LIVE_DURATION_MS = LIVE_DURATION_MS * MULTI_PERIOD_COUNT;
private static final long AVAILABILITY_START_TIME_MS = 60000; private static final long AVAILABILITY_START_TIME_MS = 60000;
private static final long AVAILABILITY_REALTIME_OFFSET_MS = 1000; private static final long AVAILABILITY_REALTIME_OFFSET_MS = 1000;
private static final long AVAILABILITY_CURRENT_TIME_MS = private static final long AVAILABILITY_CURRENT_TIME_MS =
...@@ -98,19 +104,96 @@ public class DashChunkSourceTest extends InstrumentationTestCase { ...@@ -98,19 +104,96 @@ public class DashChunkSourceTest extends InstrumentationTestCase {
assertEquals(TALL_HEIGHT, format.maxHeight); assertEquals(TALL_HEIGHT, format.maxHeight);
} }
public void testGetSeekRangeOnVod() { public void testGetAvailableRangeOnVod() {
DashChunkSource chunkSource = new DashChunkSource(generateVodMpd(), AdaptationSet.TYPE_VIDEO, DashChunkSource chunkSource = new DashChunkSource(generateVodMpd(), AdaptationSet.TYPE_VIDEO,
null, null, mock(FormatEvaluator.class)); null, null, mock(FormatEvaluator.class));
chunkSource.enable(0); chunkSource.enable(0);
TimeRange seekRange = chunkSource.getSeekRange(); TimeRange availableRange = chunkSource.getAvailableRange();
checkSeekRange(seekRange, 0, VOD_DURATION_MS * 1000); checkAvailableRange(availableRange, 0, VOD_DURATION_MS * 1000);
long[] seekRangeValuesMs = seekRange.getCurrentBoundsMs(null); long[] seekRangeValuesMs = availableRange.getCurrentBoundsMs(null);
assertEquals(0, seekRangeValuesMs[0]); assertEquals(0, seekRangeValuesMs[0]);
assertEquals(VOD_DURATION_MS, seekRangeValuesMs[1]); assertEquals(VOD_DURATION_MS, seekRangeValuesMs[1]);
} }
public void testGetAvailableRangeOnLiveWithTimelineNoEdgeLatency() {
long liveEdgeLatency = 0;
MediaPresentationDescription mpd = generateLiveMpdWithTimeline(0, 0, LIVE_DURATION_MS);
DashChunkSource chunkSource = setupDashChunkSource(mpd, 0, liveEdgeLatency);
TimeRange availableRange = chunkSource.getAvailableRange();
checkAvailableRange(availableRange, 0, LIVE_DURATION_MS * 1000);
}
public void testGetAvailableRangeOnLiveWithTimeline500msEdgeLatency() {
long liveEdgeLatency = 500;
MediaPresentationDescription mpd = generateLiveMpdWithTimeline(0, 0, LIVE_DURATION_MS);
DashChunkSource chunkSource = setupDashChunkSource(mpd, 0, liveEdgeLatency);
TimeRange availableRange = chunkSource.getAvailableRange();
checkAvailableRange(availableRange, 0, LIVE_DURATION_MS * 1000);
}
public void testGetAvailableRangeOnMultiPeriodVod() {
DashChunkSource chunkSource = new DashChunkSource(generateMultiPeriodVodMpd(),
AdaptationSet.TYPE_VIDEO, null, null, EVALUATOR);
chunkSource.enable(0);
TimeRange availableRange = chunkSource.getAvailableRange();
checkAvailableRange(availableRange, 0, MULTI_PERIOD_VOD_DURATION_MS * 1000);
}
public void testGetSeekRangeOnMultiPeriodLiveWithTimelineNoEdgeLatency() {
long liveEdgeLatency = 0;
MediaPresentationDescription mpd = generateMultiPeriodLiveMpdWithTimeline(0);
DashChunkSource chunkSource = setupDashChunkSource(mpd, 0, liveEdgeLatency);
TimeRange availableRange = chunkSource.getAvailableRange();
checkAvailableRange(availableRange, 0, MULTI_PERIOD_LIVE_DURATION_MS * 1000);
}
public void testGetSeekRangeOnMultiPeriodLiveWithTimeline500msEdgeLatency() {
long liveEdgeLatency = 500;
MediaPresentationDescription mpd = generateMultiPeriodLiveMpdWithTimeline(0);
DashChunkSource chunkSource = setupDashChunkSource(mpd, 0, liveEdgeLatency);
TimeRange availableRange = chunkSource.getAvailableRange();
checkAvailableRange(availableRange, 0, MULTI_PERIOD_LIVE_DURATION_MS * 1000);
}
public void testSegmentIndexInitializationOnVod() {
DashChunkSource chunkSource = new DashChunkSource(generateVodMpd(),
AdaptationSet.TYPE_VIDEO, null, mockDataSource, EVALUATOR);
chunkSource.enable(0);
List<MediaChunk> queue = new ArrayList<MediaChunk>();
ChunkOperationHolder out = new ChunkOperationHolder();
// request first chunk; should get back initialization chunk
chunkSource.getChunkOperation(queue, 0, 0, out);
assertNotNull(out.chunk);
assertNotNull(((InitializationChunk) out.chunk).dataSpec);
}
public void testSegmentRequestSequenceOnMultiPeriodLiveWithTimeline() {
long liveEdgeLatency = 0;
MediaPresentationDescription mpd = generateMultiPeriodLiveMpdWithTimeline(0);
DashChunkSource chunkSource = setupDashChunkSource(mpd, 0, liveEdgeLatency);
checkSegmentRequestSequenceOnMultiPeriodLive(chunkSource);
}
public void testSegmentRequestSequenceOnMultiPeriodLiveWithTemplate() {
long liveEdgeLatency = 0;
MediaPresentationDescription mpd = generateMultiPeriodLiveMpdWithTemplate(0);
DashChunkSource chunkSource = setupDashChunkSource(mpd, 0, liveEdgeLatency,
AVAILABILITY_CURRENT_TIME_MS + LIVE_DURATION_MS);
checkSegmentRequestSequenceOnMultiPeriodLive(chunkSource);
}
public void testMaxVideoDimensionsLegacy() { public void testMaxVideoDimensionsLegacy() {
SingleSegmentBase segmentBase1 = new SingleSegmentBase("https://example.com/1.mp4"); SingleSegmentBase segmentBase1 = new SingleSegmentBase("https://example.com/1.mp4");
Representation representation1 = Representation representation1 =
...@@ -131,192 +214,197 @@ public class DashChunkSourceTest extends InstrumentationTestCase { ...@@ -131,192 +214,197 @@ public class DashChunkSourceTest extends InstrumentationTestCase {
public void testLiveEdgeNoLatency() { public void testLiveEdgeNoLatency() {
long startTimeMs = 0; long startTimeMs = 0;
long liveEdgeLatencyMs = 0; long liveEdgeLatencyMs = 0;
long seekPositionMs = LIVE_SEEK_BEYOND_EDGE_MS; long seekPositionMs = startTimeMs + LIVE_DURATION_MS - liveEdgeLatencyMs;
long seekRangeStartMs = 0; long availableRangeStartMs = 0;
long seekRangeEndMs = LIVE_DURATION_MS - liveEdgeLatencyMs; long availableRangeEndMs = LIVE_DURATION_MS;
long chunkStartTimeMs = 4000; long chunkStartTimeMs = 4000;
long chunkEndTimeMs = 5000; long chunkEndTimeMs = 5000;
checkLiveTimelineConsistency(startTimeMs, liveEdgeLatencyMs, seekPositionMs, checkLiveTimelineConsistency(startTimeMs, liveEdgeLatencyMs, seekPositionMs,
seekRangeStartMs, seekRangeEndMs, chunkStartTimeMs, chunkEndTimeMs); availableRangeStartMs, availableRangeEndMs, chunkStartTimeMs, chunkEndTimeMs);
} }
public void testLiveEdgeAlmostNoLatency() { public void testLiveEdgeAlmostNoLatency() {
long startTimeMs = 0; long startTimeMs = 0;
long liveEdgeLatencyMs = 1; long liveEdgeLatencyMs = 1;
long seekPositionMs = LIVE_SEEK_BEYOND_EDGE_MS; long seekPositionMs = startTimeMs + LIVE_DURATION_MS - liveEdgeLatencyMs;
long seekRangeStartMs = 0; long availableRangeStartMs = 0;
long seekRangeEndMs = LIVE_DURATION_MS - liveEdgeLatencyMs; long availableRangeEndMs = LIVE_DURATION_MS;
long chunkStartTimeMs = 4000; long chunkStartTimeMs = 4000;
long chunkEndTimeMs = 5000; long chunkEndTimeMs = 5000;
checkLiveTimelineConsistency(startTimeMs, liveEdgeLatencyMs, seekPositionMs, checkLiveTimelineConsistency(startTimeMs, liveEdgeLatencyMs, seekPositionMs,
seekRangeStartMs, seekRangeEndMs, chunkStartTimeMs, chunkEndTimeMs); availableRangeStartMs, availableRangeEndMs, chunkStartTimeMs, chunkEndTimeMs);
} }
public void testLiveEdge500msLatency() { public void testLiveEdge500msLatency() {
long startTimeMs = 0; long startTimeMs = 0;
long liveEdgeLatencyMs = 500; long liveEdgeLatencyMs = 500;
long seekPositionMs = LIVE_SEEK_BEYOND_EDGE_MS; long seekPositionMs = startTimeMs + LIVE_DURATION_MS - liveEdgeLatencyMs;
long seekRangeStartMs = 0; long availableRangeStartMs = 0;
long seekRangeEndMs = LIVE_DURATION_MS - liveEdgeLatencyMs; long availableRangeEndMs = LIVE_DURATION_MS;
long chunkStartTimeMs = 4000; long chunkStartTimeMs = 4000;
long chunkEndTimeMs = 5000; long chunkEndTimeMs = 5000;
checkLiveTimelineConsistency(startTimeMs, liveEdgeLatencyMs, seekPositionMs, checkLiveTimelineConsistency(startTimeMs, liveEdgeLatencyMs, seekPositionMs,
seekRangeStartMs, seekRangeEndMs, chunkStartTimeMs, chunkEndTimeMs); availableRangeStartMs, availableRangeEndMs, chunkStartTimeMs, chunkEndTimeMs);
} }
public void testLiveEdge1000msLatency() { public void testLiveEdge1000msLatency() {
long startTimeMs = 0; long startTimeMs = 0;
long liveEdgeLatencyMs = 1000; long liveEdgeLatencyMs = 1000;
long seekPositionMs = LIVE_SEEK_BEYOND_EDGE_MS; long seekPositionMs = startTimeMs + LIVE_DURATION_MS - liveEdgeLatencyMs;
long seekRangeStartMs = 0; long availableRangeStartMs = 0;
long seekRangeEndMs = LIVE_DURATION_MS - liveEdgeLatencyMs; long availableRangeEndMs = LIVE_DURATION_MS;
long chunkStartTimeMs = 4000; long chunkStartTimeMs = 4000;
long chunkEndTimeMs = 5000; long chunkEndTimeMs = 5000;
checkLiveTimelineConsistency(startTimeMs, liveEdgeLatencyMs, seekPositionMs, checkLiveTimelineConsistency(startTimeMs, liveEdgeLatencyMs, seekPositionMs,
seekRangeStartMs, seekRangeEndMs, chunkStartTimeMs, chunkEndTimeMs); availableRangeStartMs, availableRangeEndMs, chunkStartTimeMs, chunkEndTimeMs);
} }
public void testLiveEdge1001msLatency() { public void testLiveEdge1001msLatency() {
long startTimeMs = 0; long startTimeMs = 0;
long liveEdgeLatencyMs = 1001; long liveEdgeLatencyMs = 1001;
long seekPositionMs = LIVE_SEEK_BEYOND_EDGE_MS; long seekPositionMs = startTimeMs + LIVE_DURATION_MS - liveEdgeLatencyMs;
long seekRangeStartMs = 0; long availableRangeStartMs = 0;
long seekRangeEndMs = LIVE_DURATION_MS - liveEdgeLatencyMs; long availableRangeEndMs = LIVE_DURATION_MS;
long chunkStartTimeMs = 3000; long chunkStartTimeMs = 3000;
long chunkEndTimeMs = 4000; long chunkEndTimeMs = 4000;
checkLiveTimelineConsistency(startTimeMs, liveEdgeLatencyMs, seekPositionMs, checkLiveTimelineConsistency(startTimeMs, liveEdgeLatencyMs, seekPositionMs,
seekRangeStartMs, seekRangeEndMs, chunkStartTimeMs, chunkEndTimeMs); availableRangeStartMs, availableRangeEndMs, chunkStartTimeMs, chunkEndTimeMs);
} }
public void testLiveEdge2500msLatency() { public void testLiveEdge2500msLatency() {
long startTimeMs = 0; long startTimeMs = 0;
long liveEdgeLatencyMs = 2500; long liveEdgeLatencyMs = 2500;
long seekPositionMs = LIVE_SEEK_BEYOND_EDGE_MS; long seekPositionMs = startTimeMs + LIVE_DURATION_MS - liveEdgeLatencyMs;
long seekRangeStartMs = 0; long availableRangeStartMs = 0;
long seekRangeEndMs = LIVE_DURATION_MS - liveEdgeLatencyMs; long availableRangeEndMs = LIVE_DURATION_MS;
long chunkStartTimeMs = 2000; long chunkStartTimeMs = 2000;
long chunkEndTimeMs = 3000; long chunkEndTimeMs = 3000;
checkLiveTimelineConsistency(startTimeMs, liveEdgeLatencyMs, seekPositionMs, checkLiveTimelineConsistency(startTimeMs, liveEdgeLatencyMs, seekPositionMs,
seekRangeStartMs, seekRangeEndMs, chunkStartTimeMs, chunkEndTimeMs); availableRangeStartMs, availableRangeEndMs, chunkStartTimeMs, chunkEndTimeMs);
} }
public void testLiveEdgeVeryHighLatency() { public void testLiveEdgeVeryHighLatency() {
long startTimeMs = 0; long startTimeMs = 0;
long liveEdgeLatencyMs = 10000; long liveEdgeLatencyMs = 10000;
long seekPositionMs = LIVE_SEEK_BEYOND_EDGE_MS; long seekPositionMs = startTimeMs + LIVE_DURATION_MS - liveEdgeLatencyMs;
long seekRangeStartMs = 0; long availableRangeStartMs = 0;
long seekRangeEndMs = 0; long availableRangeEndMs = LIVE_DURATION_MS;
long chunkStartTimeMs = 0; long chunkStartTimeMs = 0;
long chunkEndTimeMs = 1000; long chunkEndTimeMs = 1000;
checkLiveTimelineConsistency(startTimeMs, liveEdgeLatencyMs, seekPositionMs, checkLiveTimelineConsistency(startTimeMs, liveEdgeLatencyMs, seekPositionMs,
seekRangeStartMs, seekRangeEndMs, chunkStartTimeMs, chunkEndTimeMs); availableRangeStartMs, availableRangeEndMs, chunkStartTimeMs, chunkEndTimeMs);
} }
public void testLiveEdgeNoLatencyInProgress() { public void testLiveEdgeNoLatencyInProgress() {
long startTimeMs = 3000; long startTimeMs = 3000;
long liveEdgeLatencyMs = 0; long liveEdgeLatencyMs = 0;
long seekPositionMs = LIVE_SEEK_BEYOND_EDGE_MS; long seekPositionMs = startTimeMs + LIVE_DURATION_MS - liveEdgeLatencyMs;
long seekRangeStartMs = 3000; long availableRangeStartMs = 3000;
long seekRangeEndMs = 3000 + LIVE_DURATION_MS - liveEdgeLatencyMs; long availableRangeEndMs = 3000 + LIVE_DURATION_MS;
long chunkStartTimeMs = 7000; long chunkStartTimeMs = 7000;
long chunkEndTimeMs = 8000; long chunkEndTimeMs = 8000;
checkLiveTimelineConsistency(startTimeMs, liveEdgeLatencyMs, seekPositionMs, checkLiveTimelineConsistency(startTimeMs, liveEdgeLatencyMs, seekPositionMs,
seekRangeStartMs, seekRangeEndMs, chunkStartTimeMs, chunkEndTimeMs); availableRangeStartMs, availableRangeEndMs, chunkStartTimeMs, chunkEndTimeMs);
} }
public void testLiveEdgeAlmostNoLatencyInProgress() { public void testLiveEdgeAlmostNoLatencyInProgress() {
long startTimeMs = 3000; long startTimeMs = 3000;
long liveEdgeLatencyMs = 1; long liveEdgeLatencyMs = 1;
long seekPositionMs = LIVE_SEEK_BEYOND_EDGE_MS; long seekPositionMs = startTimeMs + LIVE_DURATION_MS - liveEdgeLatencyMs;
long seekRangeStartMs = 3000; long availableRangeStartMs = 3000;
long seekRangeEndMs = 3000 + LIVE_DURATION_MS - liveEdgeLatencyMs; long availableRangeEndMs = 3000 + LIVE_DURATION_MS;
long chunkStartTimeMs = 7000; long chunkStartTimeMs = 7000;
long chunkEndTimeMs = 8000; long chunkEndTimeMs = 8000;
checkLiveTimelineConsistency(startTimeMs, liveEdgeLatencyMs, seekPositionMs, checkLiveTimelineConsistency(startTimeMs, liveEdgeLatencyMs, seekPositionMs,
seekRangeStartMs, seekRangeEndMs, chunkStartTimeMs, chunkEndTimeMs); availableRangeStartMs, availableRangeEndMs, chunkStartTimeMs, chunkEndTimeMs);
} }
public void testLiveEdge500msLatencyInProgress() { public void testLiveEdge500msLatencyInProgress() {
long startTimeMs = 3000; long startTimeMs = 3000;
long liveEdgeLatencyMs = 500; long liveEdgeLatencyMs = 500;
long seekPositionMs = LIVE_SEEK_BEYOND_EDGE_MS; long seekPositionMs = startTimeMs + LIVE_DURATION_MS - liveEdgeLatencyMs;
long seekRangeStartMs = 3000; long availableRangeStartMs = 3000;
long seekRangeEndMs = 3000 + LIVE_DURATION_MS - liveEdgeLatencyMs; long availableRangeEndMs = 3000 + LIVE_DURATION_MS;
long chunkStartTimeMs = 7000; long chunkStartTimeMs = 7000;
long chunkEndTimeMs = 8000; long chunkEndTimeMs = 8000;
checkLiveTimelineConsistency(startTimeMs, liveEdgeLatencyMs, seekPositionMs, checkLiveTimelineConsistency(startTimeMs, liveEdgeLatencyMs, seekPositionMs,
seekRangeStartMs, seekRangeEndMs, chunkStartTimeMs, chunkEndTimeMs); availableRangeStartMs, availableRangeEndMs, chunkStartTimeMs, chunkEndTimeMs);
} }
public void testLiveEdge1000msLatencyInProgress() { public void testLiveEdge1000msLatencyInProgress() {
long startTimeMs = 3000; long startTimeMs = 3000;
long liveEdgeLatencyMs = 1000; long liveEdgeLatencyMs = 1000;
long seekPositionMs = LIVE_SEEK_BEYOND_EDGE_MS; long seekPositionMs = startTimeMs + LIVE_DURATION_MS - liveEdgeLatencyMs;
long seekRangeStartMs = 3000; long availableRangeStartMs = 3000;
long seekRangeEndMs = 3000 + LIVE_DURATION_MS - liveEdgeLatencyMs; long availableRangeEndMs = 3000 + LIVE_DURATION_MS;
long chunkStartTimeMs = 7000; long chunkStartTimeMs = 7000;
long chunkEndTimeMs = 8000; long chunkEndTimeMs = 8000;
checkLiveTimelineConsistency(startTimeMs, liveEdgeLatencyMs, seekPositionMs, checkLiveTimelineConsistency(startTimeMs, liveEdgeLatencyMs, seekPositionMs,
seekRangeStartMs, seekRangeEndMs, chunkStartTimeMs, chunkEndTimeMs); availableRangeStartMs, availableRangeEndMs, chunkStartTimeMs, chunkEndTimeMs);
} }
public void testLiveEdge1001msLatencyInProgress() { public void testLiveEdge1001msLatencyInProgress() {
long startTimeMs = 3000; long startTimeMs = 3000;
long liveEdgeLatencyMs = 1001; long liveEdgeLatencyMs = 1001;
long seekPositionMs = LIVE_SEEK_BEYOND_EDGE_MS; long seekPositionMs = startTimeMs + LIVE_DURATION_MS - liveEdgeLatencyMs;
long seekRangeStartMs = 3000; long availableRangeStartMs = 3000;
long seekRangeEndMs = 3000 + LIVE_DURATION_MS - liveEdgeLatencyMs; long availableRangeEndMs = 3000 + LIVE_DURATION_MS;
long chunkStartTimeMs = 6000; long chunkStartTimeMs = 6000;
long chunkEndTimeMs = 7000; long chunkEndTimeMs = 7000;
checkLiveTimelineConsistency(startTimeMs, liveEdgeLatencyMs, seekPositionMs, checkLiveTimelineConsistency(startTimeMs, liveEdgeLatencyMs, seekPositionMs,
seekRangeStartMs, seekRangeEndMs, chunkStartTimeMs, chunkEndTimeMs); availableRangeStartMs, availableRangeEndMs, chunkStartTimeMs, chunkEndTimeMs);
} }
public void testLiveEdge2500msLatencyInProgress() { public void testLiveEdge2500msLatencyInProgress() {
long startTimeMs = 3000; long startTimeMs = 3000;
long liveEdgeLatencyMs = 2500; long liveEdgeLatencyMs = 2500;
long seekPositionMs = LIVE_SEEK_BEYOND_EDGE_MS; long seekPositionMs = startTimeMs + LIVE_DURATION_MS - liveEdgeLatencyMs;
long seekRangeStartMs = 3000; long availableRangeStartMs = 3000;
long seekRangeEndMs = 3000 + LIVE_DURATION_MS - liveEdgeLatencyMs; long availableRangeEndMs = 3000 + LIVE_DURATION_MS;
long chunkStartTimeMs = 5000; long chunkStartTimeMs = 5000;
long chunkEndTimeMs = 6000; long chunkEndTimeMs = 6000;
checkLiveTimelineConsistency(startTimeMs, liveEdgeLatencyMs, seekPositionMs, checkLiveTimelineConsistency(startTimeMs, liveEdgeLatencyMs, seekPositionMs,
seekRangeStartMs, seekRangeEndMs, chunkStartTimeMs, chunkEndTimeMs); availableRangeStartMs, availableRangeEndMs, chunkStartTimeMs, chunkEndTimeMs);
} }
public void testLiveEdgeVeryHighLatencyInProgress() { public void testLiveEdgeVeryHighLatencyInProgress() {
long startTimeMs = 3000; long startTimeMs = 3000;
long liveEdgeLatencyMs = 10000; long liveEdgeLatencyMs = 10000;
long seekPositionMs = LIVE_SEEK_BEYOND_EDGE_MS; long seekPositionMs = startTimeMs + LIVE_DURATION_MS - liveEdgeLatencyMs;
long seekRangeStartMs = 3000; long availableRangeStartMs = 3000;
long seekRangeEndMs = 3000; long availableRangeEndMs = 3000 + LIVE_DURATION_MS;
long chunkStartTimeMs = 3000; long chunkStartTimeMs = 3000;
long chunkEndTimeMs = 4000; long chunkEndTimeMs = 4000;
checkLiveEdgeLatencyWithTimeline(startTimeMs, 0, liveEdgeLatencyMs, seekPositionMs, checkLiveEdgeLatencyWithTimeline(startTimeMs, 0, liveEdgeLatencyMs, seekPositionMs,
seekRangeStartMs, seekRangeEndMs, chunkStartTimeMs, chunkEndTimeMs); availableRangeStartMs, availableRangeEndMs, chunkStartTimeMs, chunkEndTimeMs);
checkLiveEdgeLatencyWithTimeline(0, startTimeMs, liveEdgeLatencyMs, seekPositionMs,
availableRangeStartMs, availableRangeEndMs, chunkStartTimeMs, chunkEndTimeMs);
checkLiveEdgeLatencyWithTemplateAndUnlimitedTimeshift(startTimeMs, liveEdgeLatencyMs, checkLiveEdgeLatencyWithTemplateAndUnlimitedTimeshift(startTimeMs, liveEdgeLatencyMs,
seekPositionMs, 0, 0, 1000); 0, availableRangeEndMs, 0, 1000);
checkLiveEdgeLatencyWithTemplateAndLimitedTimeshift(startTimeMs, liveEdgeLatencyMs, checkLiveEdgeLatencyWithTemplateAndLimitedTimeshift(startTimeMs, liveEdgeLatencyMs,
seekPositionMs, seekRangeStartMs, seekRangeEndMs, chunkStartTimeMs, chunkEndTimeMs); seekPositionMs, availableRangeStartMs, availableRangeEndMs, chunkStartTimeMs,
chunkEndTimeMs);
} }
private static Representation generateVodRepresentation(long startTimeMs, long duration, private static Representation generateVodRepresentation(long startTimeMs, long duration,
Format format) { Format format) {
SingleSegmentBase segmentBase = new SingleSegmentBase("https://example.com/1.mp4"); RangedUri rangedUri = new RangedUri("https://example.com/1.mp4", null, 0, 100);
SingleSegmentBase segmentBase = new SingleSegmentBase(rangedUri, 1, 0,
"https://example.com/1.mp4", 0, -1);
return Representation.newInstance(startTimeMs, duration, null, 0, format, segmentBase); return Representation.newInstance(startTimeMs, duration, null, 0, format, segmentBase);
} }
...@@ -341,6 +429,18 @@ public class DashChunkSourceTest extends InstrumentationTestCase { ...@@ -341,6 +429,18 @@ public class DashChunkSourceTest extends InstrumentationTestCase {
REGULAR_VIDEO, segmentBase); REGULAR_VIDEO, segmentBase);
} }
private static Representation generateSegmentTemplateRepresentation(long periodStartMs,
long periodDurationMs) {
UrlTemplate initializationTemplate = null;
UrlTemplate mediaTemplate = UrlTemplate.compile("$RepresentationID$/$Number$");
int startNumber = (int) (periodStartMs / LIVE_SEGMENT_DURATION_MS);
MultiSegmentBase segmentBase = new SegmentTemplate(null, 1000, 0,
periodDurationMs, startNumber, LIVE_SEGMENT_DURATION_MS, null,
initializationTemplate, mediaTemplate, "http://www.youtube.com");
return Representation.newInstance(periodStartMs, periodDurationMs, null, 0, REGULAR_VIDEO,
segmentBase);
}
private static MediaPresentationDescription generateMpd(boolean live, private static MediaPresentationDescription generateMpd(boolean live,
List<Representation> representations, boolean limitTimeshiftBuffer) { List<Representation> representations, boolean limitTimeshiftBuffer) {
Representation firstRepresentation = representations.get(0); Representation firstRepresentation = representations.get(0);
...@@ -354,6 +454,17 @@ public class DashChunkSourceTest extends InstrumentationTestCase { ...@@ -354,6 +454,17 @@ public class DashChunkSourceTest extends InstrumentationTestCase {
Collections.singletonList(period)); Collections.singletonList(period));
} }
private static MediaPresentationDescription generateMultiPeriodMpd(boolean live,
List<Period> periods, boolean limitTimeshiftBuffer) {
Period firstPeriod = periods.get(0);
Period lastPeriod = periods.get(periods.size() - 1);
long duration = (live) ? TrackRenderer.UNKNOWN_TIME_US
: (lastPeriod.startMs + lastPeriod.durationMs - firstPeriod.startMs);
return new MediaPresentationDescription(AVAILABILITY_START_TIME_MS, duration, -1, live, -1,
(limitTimeshiftBuffer) ? LIVE_TIMESHIFT_BUFFER_DEPTH_MS : -1,
null, null, periods);
}
private static MediaPresentationDescription generateVodMpd() { private static MediaPresentationDescription generateVodMpd() {
List<Representation> representations = new ArrayList<>(); List<Representation> representations = new ArrayList<>();
...@@ -363,103 +474,269 @@ public class DashChunkSourceTest extends InstrumentationTestCase { ...@@ -363,103 +474,269 @@ public class DashChunkSourceTest extends InstrumentationTestCase {
return generateMpd(false, representations, false); return generateMpd(false, representations, false);
} }
private MediaPresentationDescription generateMultiPeriodVodMpd() {
List<Period> periods = new ArrayList<>();
long startTimeMs = 0;
long duration = VOD_DURATION_MS;
for (int i = 0; i < 2; i++) {
Representation representation = generateVodRepresentation(startTimeMs, duration,
REGULAR_VIDEO);
AdaptationSet adaptationSet = new AdaptationSet(0, AdaptationSet.TYPE_UNKNOWN,
Collections.singletonList(representation));
Period period = new Period(null, startTimeMs, duration,
Collections.singletonList(adaptationSet));
periods.add(period);
startTimeMs += duration;
}
return generateMultiPeriodMpd(false, periods, false);
}
private static MediaPresentationDescription generateLiveMpdWithTimeline(long segmentStartMs, private static MediaPresentationDescription generateLiveMpdWithTimeline(long segmentStartMs,
long periodStartMs, long durationMs) { long periodStartMs, long durationMs) {
return generateMpd(true, Collections.singletonList(generateSegmentTimelineRepresentation( return generateMpd(true, Collections.singletonList(generateSegmentTimelineRepresentation(
segmentStartMs, periodStartMs, durationMs)), false); segmentStartMs, periodStartMs, durationMs)), false);
} }
private static MediaPresentationDescription generateLiveMpdWithTemplate( private static MediaPresentationDescription generateLiveMpdWithTemplate(long periodStartMs,
boolean limitTimeshiftBuffer) { long periodDurationMs, boolean limitTimeshiftBuffer) {
List<Representation> representations = new ArrayList<>(); return generateMpd(true, Collections.singletonList(generateSegmentTemplateRepresentation(
periodStartMs, periodDurationMs)), limitTimeshiftBuffer);
}
UrlTemplate initializationTemplate = null; private static MediaPresentationDescription generateMultiPeriodLiveMpdWithTimeline(
UrlTemplate mediaTemplate = UrlTemplate.compile("$RepresentationID$/$Number$"); long startTimeMs) {
MultiSegmentBase segmentBase = new SegmentTemplate(null, 1000, 0, List<Period> periods = new ArrayList<Period>();
TrackRenderer.UNKNOWN_TIME_US, 0, LIVE_SEGMENT_DURATION_MS, null,
initializationTemplate, mediaTemplate, "http://www.youtube.com");
Representation representation = Representation.newInstance(0, TrackRenderer.UNKNOWN_TIME_US,
null, 0, REGULAR_VIDEO, segmentBase);
representations.add(representation);
return generateMpd(true, representations, limitTimeshiftBuffer); for (int i = 0; i < MULTI_PERIOD_COUNT; i++) {
Representation representation = generateSegmentTimelineRepresentation(0, startTimeMs,
LIVE_DURATION_MS);
AdaptationSet adaptationSet = new AdaptationSet(0, AdaptationSet.TYPE_UNKNOWN,
Collections.singletonList(representation));
long duration = (i < MULTI_PERIOD_COUNT - 1) ? MULTI_PERIOD_COUNT
: TrackRenderer.END_OF_TRACK_US;
Period period = new Period(null, startTimeMs, duration,
Collections.singletonList(adaptationSet));
periods.add(period);
startTimeMs += LIVE_DURATION_MS;
}
return generateMultiPeriodMpd(true, periods, false);
}
private static MediaPresentationDescription generateMultiPeriodLiveMpdWithTemplate(
long periodStartTimeMs) {
List<Period> periods = new ArrayList<Period>();
Representation representation1 = generateSegmentTemplateRepresentation(periodStartTimeMs,
LIVE_DURATION_MS);
AdaptationSet adaptationSet1 = new AdaptationSet(0, AdaptationSet.TYPE_UNKNOWN,
Collections.singletonList(representation1));
Period period1 = new Period(null, periodStartTimeMs, LIVE_DURATION_MS,
Collections.singletonList(adaptationSet1));
periods.add(period1);
periodStartTimeMs += LIVE_DURATION_MS;
Representation representation2 = generateSegmentTemplateRepresentation(periodStartTimeMs,
TrackRenderer.UNKNOWN_TIME_US);
AdaptationSet adaptationSet2 = new AdaptationSet(0, AdaptationSet.TYPE_UNKNOWN,
Collections.singletonList(representation2));
Period period2 = new Period(null, periodStartTimeMs, TrackRenderer.UNKNOWN_TIME_US,
Collections.singletonList(adaptationSet2));
periods.add(period2);
return generateMultiPeriodMpd(true, periods, false);
} }
private DashChunkSource setupDashChunkSource(MediaPresentationDescription mpd, long periodStartMs, private DashChunkSource setupDashChunkSource(MediaPresentationDescription mpd, long periodStartMs,
long liveEdgeLatencyMs) { long liveEdgeLatencyMs) {
return setupDashChunkSource(mpd, periodStartMs, liveEdgeLatencyMs,
AVAILABILITY_CURRENT_TIME_MS + periodStartMs);
}
private DashChunkSource setupDashChunkSource(MediaPresentationDescription mpd, long periodStartMs,
long liveEdgeLatencyMs, long nowUs) {
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
ManifestFetcher<MediaPresentationDescription> manifestFetcher = mock(ManifestFetcher.class); ManifestFetcher<MediaPresentationDescription> manifestFetcher = mock(ManifestFetcher.class);
when(manifestFetcher.getManifest()).thenReturn(mpd); when(manifestFetcher.getManifest()).thenReturn(mpd);
DashChunkSource chunkSource = new DashChunkSource(manifestFetcher, mpd, DashChunkSource chunkSource = new DashChunkSource(manifestFetcher, mpd,
AdaptationSet.TYPE_VIDEO, null, mockDataSource, EVALUATOR, AdaptationSet.TYPE_VIDEO, null, mockDataSource, EVALUATOR,
new FakeClock(AVAILABILITY_CURRENT_TIME_MS + periodStartMs), liveEdgeLatencyMs * 1000, new FakeClock(nowUs), liveEdgeLatencyMs * 1000, AVAILABILITY_REALTIME_OFFSET_MS * 1000,
AVAILABILITY_REALTIME_OFFSET_MS * 1000, false, null, null); false, null, null);
chunkSource.enable(0); chunkSource.enable(0);
return chunkSource; return chunkSource;
} }
private void checkSeekRange(TimeRange seekRange, long startTimeUs, long endTimeUs) { private void checkAvailableRange(TimeRange seekRange, long startTimeUs, long endTimeUs) {
long[] seekRangeValuesUs = seekRange.getCurrentBoundsUs(null); long[] seekRangeValuesUs = seekRange.getCurrentBoundsUs(null);
assertEquals(startTimeUs, seekRangeValuesUs[0]); assertEquals(startTimeUs, seekRangeValuesUs[0]);
assertEquals(endTimeUs, seekRangeValuesUs[1]); assertEquals(endTimeUs, seekRangeValuesUs[1]);
} }
private void checkLiveEdgeLatency(DashChunkSource chunkSource, List<MediaChunk> queue, private void checkLiveEdgeLatency(DashChunkSource chunkSource, List<MediaChunk> queue,
ChunkOperationHolder out, long seekPositionMs, long seekRangeStartMs, long seekRangeEndMs, ChunkOperationHolder out, long seekPositionMs, long availableRangeStartMs,
long chunkStartTimeMs, long chunkEndTimeMs) { long availableRangeEndMs, long chunkStartTimeMs, long chunkEndTimeMs) {
chunkSource.getChunkOperation(queue, seekPositionMs * 1000, 0, out); chunkSource.getChunkOperation(queue, seekPositionMs * 1000, 0, out);
TimeRange seekRange = chunkSource.getSeekRange(); TimeRange availableRange = chunkSource.getAvailableRange();
assertNotNull(out.chunk); checkAvailableRange(availableRange, availableRangeStartMs * 1000, availableRangeEndMs * 1000);
checkSeekRange(seekRange, seekRangeStartMs * 1000, seekRangeEndMs * 1000); if (chunkStartTimeMs < availableRangeEndMs) {
assertEquals(chunkStartTimeMs * 1000, ((MediaChunk) out.chunk).startTimeUs); assertNotNull(out.chunk);
assertEquals(chunkEndTimeMs * 1000, ((MediaChunk) out.chunk).endTimeUs); assertEquals(chunkStartTimeMs * 1000, ((MediaChunk) out.chunk).startTimeUs);
assertEquals(chunkEndTimeMs * 1000, ((MediaChunk) out.chunk).endTimeUs);
} else {
assertNull(out.chunk);
}
} }
private void checkLiveEdgeLatency(MediaPresentationDescription mpd, long periodStartMs, private void checkLiveEdgeLatency(MediaPresentationDescription mpd, long periodStartMs,
long liveEdgeLatencyMs, long seekPositionMs, long seekRangeStartMs, long seekRangeEndMs, long liveEdgeLatencyMs, long seekPositionMs, long availableRangeStartMs,
long chunkStartTimeMs, long chunkEndTimeMs) { long availableRangeEndMs, long chunkStartTimeMs, long chunkEndTimeMs) {
DashChunkSource chunkSource = setupDashChunkSource(mpd, periodStartMs, liveEdgeLatencyMs); DashChunkSource chunkSource = setupDashChunkSource(mpd, periodStartMs, liveEdgeLatencyMs);
List<MediaChunk> queue = new ArrayList<>(); List<MediaChunk> queue = new ArrayList<>();
ChunkOperationHolder out = new ChunkOperationHolder(); ChunkOperationHolder out = new ChunkOperationHolder();
checkLiveEdgeLatency(chunkSource, queue, out, seekPositionMs, seekRangeStartMs, seekRangeEndMs, checkLiveEdgeLatency(chunkSource, queue, out, seekPositionMs, availableRangeStartMs,
chunkStartTimeMs, chunkEndTimeMs); availableRangeEndMs, chunkStartTimeMs, chunkEndTimeMs);
} }
private void checkLiveEdgeLatencyWithTimeline(long segmentStartMs, long periodStartMs, private void checkLiveEdgeLatencyWithTimeline(long segmentStartMs, long periodStartMs,
long liveEdgeLatencyMs, long seekPositionMs, long seekRangeStartMs, long seekRangeEndMs, long liveEdgeLatencyMs, long seekPositionMs, long availableRangeStartMs,
long chunkStartTimeMs, long chunkEndTimeMs) { long availableRangeEndMs, long chunkStartTimeMs, long chunkEndTimeMs) {
MediaPresentationDescription mpd = generateLiveMpdWithTimeline(segmentStartMs, periodStartMs, MediaPresentationDescription mpd = generateLiveMpdWithTimeline(segmentStartMs, periodStartMs,
LIVE_DURATION_MS); LIVE_DURATION_MS);
checkLiveEdgeLatency(mpd, periodStartMs, liveEdgeLatencyMs, seekPositionMs, seekRangeStartMs, checkLiveEdgeLatency(mpd, periodStartMs, liveEdgeLatencyMs, seekPositionMs,
seekRangeEndMs, chunkStartTimeMs, chunkEndTimeMs); availableRangeStartMs, availableRangeEndMs, chunkStartTimeMs, chunkEndTimeMs);
} }
private void checkLiveEdgeLatencyWithTemplateAndUnlimitedTimeshift(long startTimeMs, private void checkLiveEdgeLatencyWithTemplateAndUnlimitedTimeshift(long startTimeMs,
long liveEdgeLatencyMs, long seekPositionMs, long seekRangeEndMs, long liveEdgeLatencyMs, long availablePositionMs, long availableRangeEndMs,
long chunkStartTimeMs, long chunkEndTimeMs) { long chunkStartTimeMs, long chunkEndTimeMs) {
MediaPresentationDescription mpd = generateLiveMpdWithTemplate(false); MediaPresentationDescription mpd = generateLiveMpdWithTemplate(0,
checkLiveEdgeLatency(mpd, startTimeMs, liveEdgeLatencyMs, seekPositionMs, 0, seekRangeEndMs, TrackRenderer.UNKNOWN_TIME_US, false);
chunkStartTimeMs, chunkEndTimeMs); checkLiveEdgeLatency(mpd, startTimeMs, liveEdgeLatencyMs, availablePositionMs, 0,
availableRangeEndMs, chunkStartTimeMs, chunkEndTimeMs);
} }
private void checkLiveEdgeLatencyWithTemplateAndLimitedTimeshift(long startTimeMs, private void checkLiveEdgeLatencyWithTemplateAndLimitedTimeshift(long startTimeMs,
long liveEdgeLatencyMs, long seekPositionMs, long seekRangeStartMs, long seekRangeEndMs, long liveEdgeLatencyMs, long seekPositionMs, long availableRangeStartMs,
long chunkStartTimeMs, long chunkEndTimeMs) { long availableRangeEndMs, long chunkStartTimeMs, long chunkEndTimeMs) {
MediaPresentationDescription mpd = generateLiveMpdWithTemplate(true); MediaPresentationDescription mpd = generateLiveMpdWithTemplate(0,
checkLiveEdgeLatency(mpd, startTimeMs, liveEdgeLatencyMs, seekPositionMs, seekRangeStartMs, TrackRenderer.UNKNOWN_TIME_US, true);
seekRangeEndMs, chunkStartTimeMs, chunkEndTimeMs); checkLiveEdgeLatency(mpd, startTimeMs, liveEdgeLatencyMs, seekPositionMs, availableRangeStartMs,
availableRangeEndMs, chunkStartTimeMs, chunkEndTimeMs);
} }
private void checkLiveTimelineConsistency(long startTimeMs, long liveEdgeLatencyMs, private void checkLiveTimelineConsistency(long startTimeMs, long liveEdgeLatencyMs,
long seekPositionMs, long seekRangeStartMs, long seekRangeEndMs, long chunkStartTimeMs, long seekPositionMs, long availableRangeStartMs, long availableRangeEndMs,
long chunkEndTimeMs) { long chunkStartTimeMs, long chunkEndTimeMs) {
// check the standard live-MPD style in which the period starts at time 0 and the segments
// start at startTimeMs
checkLiveEdgeLatencyWithTimeline(startTimeMs, 0, liveEdgeLatencyMs, seekPositionMs, checkLiveEdgeLatencyWithTimeline(startTimeMs, 0, liveEdgeLatencyMs, seekPositionMs,
seekRangeStartMs, seekRangeEndMs, chunkStartTimeMs, chunkEndTimeMs); availableRangeStartMs, availableRangeEndMs, chunkStartTimeMs, chunkEndTimeMs);
// check the other live-MPD style in which the segments start at time 0 and the period starts
// at startTimeMs
checkLiveEdgeLatencyWithTimeline(0, startTimeMs, liveEdgeLatencyMs, seekPositionMs,
availableRangeStartMs, availableRangeEndMs, chunkStartTimeMs, chunkEndTimeMs);
checkLiveEdgeLatencyWithTemplateAndUnlimitedTimeshift(startTimeMs, liveEdgeLatencyMs, checkLiveEdgeLatencyWithTemplateAndUnlimitedTimeshift(startTimeMs, liveEdgeLatencyMs,
seekPositionMs, seekRangeEndMs, chunkStartTimeMs, chunkEndTimeMs); seekPositionMs, availableRangeEndMs, chunkStartTimeMs, chunkEndTimeMs);
checkLiveEdgeLatencyWithTemplateAndLimitedTimeshift(startTimeMs, liveEdgeLatencyMs, checkLiveEdgeLatencyWithTemplateAndLimitedTimeshift(startTimeMs, liveEdgeLatencyMs,
seekPositionMs, seekRangeStartMs, seekRangeEndMs, chunkStartTimeMs, chunkEndTimeMs); seekPositionMs, availableRangeStartMs, availableRangeEndMs, chunkStartTimeMs,
chunkEndTimeMs);
}
private void checkSegmentRequestSequenceOnMultiPeriodLive(DashChunkSource chunkSource) {
List<MediaChunk> queue = new ArrayList<MediaChunk>();
ChunkOperationHolder out = new ChunkOperationHolder();
long seekPositionMs = 0;
long availableRangeStartMs = 0;
long availableRangeEndMs = MULTI_PERIOD_LIVE_DURATION_MS;
long chunkStartTimeMs = 0;
long chunkEndTimeMs = 1000;
// request first chunk
checkLiveEdgeLatency(chunkSource, queue, out, seekPositionMs,
availableRangeStartMs, availableRangeEndMs, chunkStartTimeMs, chunkEndTimeMs);
queue.add((MediaChunk) out.chunk);
// request second chunk
chunkStartTimeMs += 1000;
chunkEndTimeMs += 1000;
out.chunk = null;
checkLiveEdgeLatency(chunkSource, queue, out, seekPositionMs,
availableRangeStartMs, availableRangeEndMs, chunkStartTimeMs, chunkEndTimeMs);
queue.add((MediaChunk) out.chunk);
// request third chunk
chunkStartTimeMs += 1000;
chunkEndTimeMs += 1000;
out.chunk = null;
checkLiveEdgeLatency(chunkSource, queue, out, seekPositionMs,
availableRangeStartMs, availableRangeEndMs, chunkStartTimeMs, chunkEndTimeMs);
queue.add((MediaChunk) out.chunk);
// request fourth chunk
chunkStartTimeMs += 1000;
chunkEndTimeMs += 1000;
out.chunk = null;
checkLiveEdgeLatency(chunkSource, queue, out, seekPositionMs,
availableRangeStartMs, availableRangeEndMs, chunkStartTimeMs, chunkEndTimeMs);
queue.add((MediaChunk) out.chunk);
// request fifth chunk
chunkStartTimeMs += 1000;
chunkEndTimeMs += 1000;
out.chunk = null;
checkLiveEdgeLatency(chunkSource, queue, out, seekPositionMs,
availableRangeStartMs, availableRangeEndMs, chunkStartTimeMs, chunkEndTimeMs);
queue.add((MediaChunk) out.chunk);
// request sixth chunk; this is the first chunk in the 2nd period
chunkStartTimeMs += 1000;
chunkEndTimeMs += 1000;
out.chunk = null;
checkLiveEdgeLatency(chunkSource, queue, out, seekPositionMs,
availableRangeStartMs, availableRangeEndMs, chunkStartTimeMs, chunkEndTimeMs);
queue.add((MediaChunk) out.chunk);
// request seventh chunk;
chunkStartTimeMs += 1000;
chunkEndTimeMs += 1000;
out.chunk = null;
checkLiveEdgeLatency(chunkSource, queue, out, seekPositionMs,
availableRangeStartMs, availableRangeEndMs, chunkStartTimeMs, chunkEndTimeMs);
queue.add((MediaChunk) out.chunk);
// request eigth chunk
chunkStartTimeMs += 1000;
chunkEndTimeMs += 1000;
out.chunk = null;
checkLiveEdgeLatency(chunkSource, queue, out, seekPositionMs,
availableRangeStartMs, availableRangeEndMs, chunkStartTimeMs, chunkEndTimeMs);
queue.add((MediaChunk) out.chunk);
// request ninth chunk
chunkStartTimeMs += 1000;
chunkEndTimeMs += 1000;
out.chunk = null;
checkLiveEdgeLatency(chunkSource, queue, out, seekPositionMs,
availableRangeStartMs, availableRangeEndMs, chunkStartTimeMs, chunkEndTimeMs);
queue.add((MediaChunk) out.chunk);
// request tenth chunk
chunkStartTimeMs += 1000;
chunkEndTimeMs += 1000;
out.chunk = null;
checkLiveEdgeLatency(chunkSource, queue, out, seekPositionMs,
availableRangeStartMs, availableRangeEndMs, chunkStartTimeMs, chunkEndTimeMs);
queue.add((MediaChunk) out.chunk);
// request "eleventh" chunk; this chunk isn't available yet, so we should get null
out.chunk = null;
chunkSource.getChunkOperation(queue, seekPositionMs * 1000, 0, out);
assertNull(out.chunk);
} }
} }
...@@ -38,6 +38,13 @@ public abstract class BaseMediaChunk extends MediaChunk { ...@@ -38,6 +38,13 @@ public abstract class BaseMediaChunk extends MediaChunk {
private DefaultTrackOutput output; private DefaultTrackOutput output;
private int firstSampleIndex; private int firstSampleIndex;
public BaseMediaChunk(DataSource dataSource, DataSpec dataSpec, int trigger, Format format,
long startTimeUs, long endTimeUs, int chunkIndex, boolean isLastChunk,
boolean isMediaFormatFinal) {
this(dataSource, dataSpec, trigger, format, startTimeUs, endTimeUs, chunkIndex, isLastChunk,
isMediaFormatFinal, Chunk.NO_PARENT_ID);
}
/** /**
* @param dataSource A {@link DataSource} for loading the data. * @param dataSource A {@link DataSource} for loading the data.
* @param dataSpec Defines the data to be loaded. * @param dataSpec Defines the data to be loaded.
...@@ -51,11 +58,13 @@ public abstract class BaseMediaChunk extends MediaChunk { ...@@ -51,11 +58,13 @@ public abstract class BaseMediaChunk extends MediaChunk {
* be called at any time to obtain the media format and drm initialization data. False if * be called at any time to obtain the media format and drm initialization data. False if
* these methods are only guaranteed to return correct data after the first sample data has * these methods are only guaranteed to return correct data after the first sample data has
* been output from the chunk. * been output from the chunk.
* @param parentId Identifier for a parent from which this chunk originates.
*/ */
public BaseMediaChunk(DataSource dataSource, DataSpec dataSpec, int trigger, Format format, public BaseMediaChunk(DataSource dataSource, DataSpec dataSpec, int trigger, Format format,
long startTimeUs, long endTimeUs, int chunkIndex, boolean isLastChunk, long startTimeUs, long endTimeUs, int chunkIndex, boolean isLastChunk,
boolean isMediaFormatFinal) { boolean isMediaFormatFinal, int parentId) {
super(dataSource, dataSpec, trigger, format, startTimeUs, endTimeUs, chunkIndex, isLastChunk); super(dataSource, dataSpec, trigger, format, startTimeUs, endTimeUs, chunkIndex, isLastChunk,
parentId);
this.isMediaFormatFinal = isMediaFormatFinal; this.isMediaFormatFinal = isMediaFormatFinal;
} }
......
...@@ -75,6 +75,10 @@ public abstract class Chunk implements Loadable { ...@@ -75,6 +75,10 @@ public abstract class Chunk implements Loadable {
* Implementations may define custom {@link #trigger} codes greater than or equal to this value. * Implementations may define custom {@link #trigger} codes greater than or equal to this value.
*/ */
public static final int TRIGGER_CUSTOM_BASE = 10000; public static final int TRIGGER_CUSTOM_BASE = 10000;
/**
* Value of {@link #parentId} if no parent id need be specified.
*/
public static final int NO_PARENT_ID = -1;
/** /**
* The type of the chunk. For reporting only. * The type of the chunk. For reporting only.
...@@ -93,9 +97,17 @@ public abstract class Chunk implements Loadable { ...@@ -93,9 +97,17 @@ public abstract class Chunk implements Loadable {
* The {@link DataSpec} that defines the data to be loaded. * The {@link DataSpec} that defines the data to be loaded.
*/ */
public final DataSpec dataSpec; public final DataSpec dataSpec;
/**
* Optional identifier for a parent from which this chunk originates.
*/
public final int parentId;
protected final DataSource dataSource; protected final DataSource dataSource;
public Chunk(DataSource dataSource, DataSpec dataSpec, int type, int trigger, Format format) {
this(dataSource, dataSpec, type, trigger, format, NO_PARENT_ID);
}
/** /**
* @param dataSource The source from which the data should be loaded. * @param dataSource The source from which the data should be loaded.
* @param dataSpec Defines the data to be loaded. {@code dataSpec.length} must not exceed * @param dataSpec Defines the data to be loaded. {@code dataSpec.length} must not exceed
...@@ -105,13 +117,16 @@ public abstract class Chunk implements Loadable { ...@@ -105,13 +117,16 @@ public abstract class Chunk implements Loadable {
* @param type See {@link #type}. * @param type See {@link #type}.
* @param trigger See {@link #trigger}. * @param trigger See {@link #trigger}.
* @param format See {@link #format}. * @param format See {@link #format}.
* @param parentId See {@link #parentId}.
*/ */
public Chunk(DataSource dataSource, DataSpec dataSpec, int type, int trigger, Format format) { public Chunk(DataSource dataSource, DataSpec dataSpec, int type, int trigger, Format format,
int parentId) {
this.dataSource = Assertions.checkNotNull(dataSource); this.dataSource = Assertions.checkNotNull(dataSource);
this.dataSpec = Assertions.checkNotNull(dataSpec); this.dataSpec = Assertions.checkNotNull(dataSpec);
this.type = type; this.type = type;
this.trigger = trigger; this.trigger = trigger;
this.format = format; this.format = format;
this.parentId = parentId;
} }
/** /**
......
...@@ -43,6 +43,15 @@ public class ContainerMediaChunk extends BaseMediaChunk implements SingleTrackOu ...@@ -43,6 +43,15 @@ public class ContainerMediaChunk extends BaseMediaChunk implements SingleTrackOu
private volatile int bytesLoaded; private volatile int bytesLoaded;
private volatile boolean loadCanceled; private volatile boolean loadCanceled;
public ContainerMediaChunk(DataSource dataSource, DataSpec dataSpec, int trigger, Format format,
long startTimeUs, long endTimeUs, int chunkIndex, boolean isLastChunk, long sampleOffsetUs,
ChunkExtractorWrapper extractorWrapper, MediaFormat mediaFormat, DrmInitData drmInitData,
boolean isMediaFormatFinal) {
this(dataSource, dataSpec, trigger, format, startTimeUs, endTimeUs, chunkIndex, isLastChunk,
sampleOffsetUs, extractorWrapper, mediaFormat, drmInitData, isMediaFormatFinal,
Chunk.NO_PARENT_ID);
}
/** /**
* @param dataSource A {@link DataSource} for loading the data. * @param dataSource A {@link DataSource} for loading the data.
* @param dataSpec Defines the data to be loaded. * @param dataSpec Defines the data to be loaded.
...@@ -60,13 +69,14 @@ public class ContainerMediaChunk extends BaseMediaChunk implements SingleTrackOu ...@@ -60,13 +69,14 @@ public class ContainerMediaChunk extends BaseMediaChunk implements SingleTrackOu
* protected. May also be null if the data is known to define its own initialization data. * protected. May also be null if the data is known to define its own initialization data.
* @param isMediaFormatFinal True if {@code mediaFormat} and {@code drmInitData} are known to be * @param isMediaFormatFinal True if {@code mediaFormat} and {@code drmInitData} are known to be
* correct and final. False if the data may define its own format or initialization data. * correct and final. False if the data may define its own format or initialization data.
* @param parentId Identifier for a parent from which this chunk originates.
*/ */
public ContainerMediaChunk(DataSource dataSource, DataSpec dataSpec, int trigger, Format format, public ContainerMediaChunk(DataSource dataSource, DataSpec dataSpec, int trigger, Format format,
long startTimeUs, long endTimeUs, int chunkIndex, boolean isLastChunk, long sampleOffsetUs, long startTimeUs, long endTimeUs, int chunkIndex, boolean isLastChunk, long sampleOffsetUs,
ChunkExtractorWrapper extractorWrapper, MediaFormat mediaFormat, DrmInitData drmInitData, ChunkExtractorWrapper extractorWrapper, MediaFormat mediaFormat, DrmInitData drmInitData,
boolean isMediaFormatFinal) { boolean isMediaFormatFinal, int parentId) {
super(dataSource, dataSpec, trigger, format, startTimeUs, endTimeUs, chunkIndex, isLastChunk, super(dataSource, dataSpec, trigger, format, startTimeUs, endTimeUs, chunkIndex, isLastChunk,
isMediaFormatFinal); isMediaFormatFinal, parentId);
this.extractorWrapper = extractorWrapper; this.extractorWrapper = extractorWrapper;
this.sampleOffsetUs = sampleOffsetUs; this.sampleOffsetUs = sampleOffsetUs;
this.mediaFormat = getAdjustedMediaFormat(mediaFormat, sampleOffsetUs); this.mediaFormat = getAdjustedMediaFormat(mediaFormat, sampleOffsetUs);
......
...@@ -46,6 +46,11 @@ public final class InitializationChunk extends Chunk implements SingleTrackOutpu ...@@ -46,6 +46,11 @@ public final class InitializationChunk extends Chunk implements SingleTrackOutpu
private volatile int bytesLoaded; private volatile int bytesLoaded;
private volatile boolean loadCanceled; private volatile boolean loadCanceled;
public InitializationChunk(DataSource dataSource, DataSpec dataSpec, int trigger, Format format,
ChunkExtractorWrapper extractorWrapper) {
this(dataSource, dataSpec, trigger, format, extractorWrapper, Chunk.NO_PARENT_ID);
}
/** /**
* Constructor for a chunk of media samples. * Constructor for a chunk of media samples.
* *
...@@ -54,10 +59,11 @@ public final class InitializationChunk extends Chunk implements SingleTrackOutpu ...@@ -54,10 +59,11 @@ public final class InitializationChunk extends Chunk implements SingleTrackOutpu
* @param trigger The reason for this chunk being selected. * @param trigger The reason for this chunk being selected.
* @param format The format of the stream to which this chunk belongs. * @param format The format of the stream to which this chunk belongs.
* @param extractorWrapper A wrapped extractor to use for parsing the initialization data. * @param extractorWrapper A wrapped extractor to use for parsing the initialization data.
* @param parentId Identifier for a parent from which this chunk originates.
*/ */
public InitializationChunk(DataSource dataSource, DataSpec dataSpec, int trigger, Format format, public InitializationChunk(DataSource dataSource, DataSpec dataSpec, int trigger, Format format,
ChunkExtractorWrapper extractorWrapper) { ChunkExtractorWrapper extractorWrapper, int parentId) {
super(dataSource, dataSpec, Chunk.TYPE_MEDIA_INITIALIZATION, trigger, format); super(dataSource, dataSpec, Chunk.TYPE_MEDIA_INITIALIZATION, trigger, format, parentId);
this.extractorWrapper = extractorWrapper; this.extractorWrapper = extractorWrapper;
} }
......
...@@ -41,6 +41,12 @@ public abstract class MediaChunk extends Chunk { ...@@ -41,6 +41,12 @@ public abstract class MediaChunk extends Chunk {
*/ */
public final boolean isLastChunk; public final boolean isLastChunk;
public MediaChunk(DataSource dataSource, DataSpec dataSpec, int trigger, Format format,
long startTimeUs, long endTimeUs, int chunkIndex, boolean isLastChunk) {
this(dataSource, dataSpec, trigger, format, startTimeUs, endTimeUs, chunkIndex, isLastChunk,
Chunk.NO_PARENT_ID);
}
/** /**
* @param dataSource A {@link DataSource} for loading the data. * @param dataSource A {@link DataSource} for loading the data.
* @param dataSpec Defines the data to be loaded. * @param dataSpec Defines the data to be loaded.
...@@ -50,10 +56,11 @@ public abstract class MediaChunk extends Chunk { ...@@ -50,10 +56,11 @@ public abstract class MediaChunk extends Chunk {
* @param endTimeUs The end time of the media contained by the chunk, in microseconds. * @param endTimeUs The end time of the media contained by the chunk, in microseconds.
* @param chunkIndex The index of the chunk. * @param chunkIndex The index of the chunk.
* @param isLastChunk True if this is the last chunk in the media. False otherwise. * @param isLastChunk True if this is the last chunk in the media. False otherwise.
* @param parentId Identifier for a parent from which this chunk originates.
*/ */
public MediaChunk(DataSource dataSource, DataSpec dataSpec, int trigger, Format format, public MediaChunk(DataSource dataSource, DataSpec dataSpec, int trigger, Format format,
long startTimeUs, long endTimeUs, int chunkIndex, boolean isLastChunk) { long startTimeUs, long endTimeUs, int chunkIndex, boolean isLastChunk, int parentId) {
super(dataSource, dataSpec, Chunk.TYPE_MEDIA, trigger, format); super(dataSource, dataSpec, Chunk.TYPE_MEDIA, trigger, format, parentId);
Assertions.checkNotNull(format); Assertions.checkNotNull(format);
this.startTimeUs = startTimeUs; this.startTimeUs = startTimeUs;
this.endTimeUs = endTimeUs; this.endTimeUs = endTimeUs;
......
...@@ -35,6 +35,13 @@ public final class SingleSampleMediaChunk extends BaseMediaChunk { ...@@ -35,6 +35,13 @@ public final class SingleSampleMediaChunk extends BaseMediaChunk {
private volatile int bytesLoaded; private volatile int bytesLoaded;
private volatile boolean loadCanceled; private volatile boolean loadCanceled;
public SingleSampleMediaChunk(DataSource dataSource, DataSpec dataSpec, int trigger,
Format format, long startTimeUs, long endTimeUs, int chunkIndex, boolean isLastChunk,
MediaFormat sampleFormat, DrmInitData sampleDrmInitData) {
this(dataSource, dataSpec, trigger, format, startTimeUs, endTimeUs, chunkIndex, isLastChunk,
sampleFormat, sampleDrmInitData, Chunk.NO_PARENT_ID);
}
/** /**
* @param dataSource A {@link DataSource} for loading the data. * @param dataSource A {@link DataSource} for loading the data.
* @param dataSpec Defines the data to be loaded. * @param dataSpec Defines the data to be loaded.
...@@ -47,12 +54,13 @@ public final class SingleSampleMediaChunk extends BaseMediaChunk { ...@@ -47,12 +54,13 @@ public final class SingleSampleMediaChunk extends BaseMediaChunk {
* @param sampleFormat The format of the sample. * @param sampleFormat The format of the sample.
* @param sampleDrmInitData The {@link DrmInitData} for the sample. Null if the sample is not drm * @param sampleDrmInitData The {@link DrmInitData} for the sample. Null if the sample is not drm
* protected. * protected.
* @param parentId Identifier for a parent from which this chunk originates.
*/ */
public SingleSampleMediaChunk(DataSource dataSource, DataSpec dataSpec, int trigger, public SingleSampleMediaChunk(DataSource dataSource, DataSpec dataSpec, int trigger,
Format format, long startTimeUs, long endTimeUs, int chunkIndex, boolean isLastChunk, Format format, long startTimeUs, long endTimeUs, int chunkIndex, boolean isLastChunk,
MediaFormat sampleFormat, DrmInitData sampleDrmInitData) { MediaFormat sampleFormat, DrmInitData sampleDrmInitData, int parentId) {
super(dataSource, dataSpec, trigger, format, startTimeUs, endTimeUs, chunkIndex, isLastChunk, super(dataSource, dataSpec, trigger, format, startTimeUs, endTimeUs, chunkIndex, isLastChunk,
true); true, parentId);
this.sampleFormat = sampleFormat; this.sampleFormat = sampleFormat;
this.sampleDrmInitData = sampleDrmInitData; this.sampleDrmInitData = sampleDrmInitData;
} }
......
...@@ -50,6 +50,7 @@ import com.google.android.exoplayer.util.MimeTypes; ...@@ -50,6 +50,7 @@ import com.google.android.exoplayer.util.MimeTypes;
import com.google.android.exoplayer.util.SystemClock; import com.google.android.exoplayer.util.SystemClock;
import android.os.Handler; import android.os.Handler;
import android.util.SparseArray;
import java.io.IOException; import java.io.IOException;
import java.util.Arrays; import java.util.Arrays;
...@@ -61,7 +62,15 @@ import java.util.List; ...@@ -61,7 +62,15 @@ import java.util.List;
* An {@link ChunkSource} for DASH streams. * An {@link ChunkSource} for DASH streams.
* <p> * <p>
* This implementation currently supports fMP4, webm, and webvtt. * This implementation currently supports fMP4, webm, and webvtt.
* <p>
* This implementation makes the following assumptions about multi-period manifests:
* <ol>
* <li>that new periods will contain the same representations as previous periods (i.e. no new or
* missing representations) and</li>
* <li>that representations are contiguous across multiple periods</li>
* </ol>
*/ */
// TODO: handle cases where the above assumption are false
public class DashChunkSource implements ChunkSource { public class DashChunkSource implements ChunkSource {
/** /**
...@@ -72,9 +81,9 @@ public class DashChunkSource implements ChunkSource { ...@@ -72,9 +81,9 @@ public class DashChunkSource implements ChunkSource {
/** /**
* Invoked when the available seek range of the stream has changed. * Invoked when the available seek range of the stream has changed.
* *
* @param seekRange The range which specifies available content that can be seeked to. * @param availableRange The range which specifies available content that can be seeked to.
*/ */
public void onSeekRangeChanged(TimeRange seekRange); public void onAvailableRangeChanged(TimeRange availableRange);
} }
...@@ -107,21 +116,19 @@ public class DashChunkSource implements ChunkSource { ...@@ -107,21 +116,19 @@ public class DashChunkSource implements ChunkSource {
private final int maxWidth; private final int maxWidth;
private final int maxHeight; private final int maxHeight;
private final Format[] formats; private final SparseArray<PeriodHolder> periodHolders;
private final HashMap<String, RepresentationHolder> representationHolders;
private final ManifestFetcher<MediaPresentationDescription> manifestFetcher; private final ManifestFetcher<MediaPresentationDescription> manifestFetcher;
private final int adaptationSetIndex; private final int adaptationSetIndex;
private final int[] representationIndices; private final int[] representationIndices;
private MediaPresentationDescription currentManifest; private MediaPresentationDescription currentManifest;
private boolean finishedCurrentManifest;
private int periodHolderNextIndex;
private DrmInitData drmInitData; private DrmInitData drmInitData;
private TimeRange seekRange; private TimeRange availableRange;
private long[] seekRangeValues; private long[] availableRangeValues;
private int firstAvailableSegmentNum;
private int lastAvailableSegmentNum;
private boolean startAtLiveEdge; private boolean startAtLiveEdge;
private boolean lastChunkWasInitialization; private boolean lastChunkWasInitialization;
...@@ -255,33 +262,36 @@ public class DashChunkSource implements ChunkSource { ...@@ -255,33 +262,36 @@ public class DashChunkSource implements ChunkSource {
this.eventHandler = eventHandler; this.eventHandler = eventHandler;
this.eventListener = eventListener; this.eventListener = eventListener;
this.evaluation = new Evaluation(); this.evaluation = new Evaluation();
this.seekRangeValues = new long[2]; this.availableRangeValues = new long[2];
drmInitData = getDrmInitData(currentManifest, adaptationSetIndex); drmInitData = getDrmInitData(currentManifest, adaptationSetIndex);
Representation[] representations = getFilteredRepresentations(currentManifest, periodHolders = new SparseArray<>();
adaptationSetIndex, representationIndices);
long periodDurationUs = (representations[0].periodDurationMs == TrackRenderer.UNKNOWN_TIME_US)
? TrackRenderer.UNKNOWN_TIME_US : representations[0].periodDurationMs * 1000;
// TODO: Remove this and pass proper formats instead (b/22996976).
this.mediaFormat = MediaFormat.createFormatForMimeType(getMediaMimeType(representations[0]),
MediaFormat.NO_VALUE, periodDurationUs);
this.formats = new Format[representations.length]; processManifest(currentManifest);
this.representationHolders = new HashMap<>();
int maxWidth = 0; String mimeType = "";
long totalDurationUs = 0;
int maxHeight = 0; int maxHeight = 0;
for (int i = 0; i < representations.length; i++) { int maxWidth = 0;
formats[i] = representations[i].format;
maxWidth = Math.max(formats[i].width, maxWidth); for (int i = 0; i < periodHolders.size(); i++) {
maxHeight = Math.max(formats[i].height, maxHeight); PeriodHolder periodHolder = periodHolders.valueAt(i);
Extractor extractor = mimeTypeIsWebm(formats[i].mimeType) ? new WebmExtractor() if (totalDurationUs != TrackRenderer.UNKNOWN_TIME_US) {
: new FragmentedMp4Extractor(); if (periodHolder.durationUs == TrackRenderer.UNKNOWN_TIME_US) {
representationHolders.put(formats[i].id, totalDurationUs = TrackRenderer.UNKNOWN_TIME_US;
new RepresentationHolder(representations[i], new ChunkExtractorWrapper(extractor))); } else {
totalDurationUs += periodHolder.durationUs;
}
}
mimeType = periodHolder.mimeType;
maxHeight = Math.max(maxHeight, periodHolder.maxHeight);
maxWidth = Math.max(maxWidth, periodHolder.maxWidth);
} }
this.maxWidth = maxWidth; // TODO: Remove this and pass proper formats instead (b/22996976).
this.mediaFormat = MediaFormat.createFormatForMimeType(mimeType, MediaFormat.NO_VALUE,
totalDurationUs);
this.maxHeight = maxHeight; this.maxHeight = maxHeight;
Arrays.sort(formats, new DecreasingBandwidthComparator()); this.maxWidth = maxWidth;
} }
@Override @Override
...@@ -306,8 +316,8 @@ public class DashChunkSource implements ChunkSource { ...@@ -306,8 +316,8 @@ public class DashChunkSource implements ChunkSource {
} }
// VisibleForTesting // VisibleForTesting
/* package */ TimeRange getSeekRange() { /* package */ TimeRange getAvailableRange() {
return seekRange; return availableRange;
} }
@Override @Override
...@@ -317,16 +327,7 @@ public class DashChunkSource implements ChunkSource { ...@@ -317,16 +327,7 @@ public class DashChunkSource implements ChunkSource {
if (manifestFetcher != null) { if (manifestFetcher != null) {
manifestFetcher.enable(); manifestFetcher.enable();
} }
DashSegmentIndex segmentIndex = updateAvailableBounds(getNowUs());
representationHolders.get(formats[0].id).representation.getIndex();
if (segmentIndex == null) {
seekRange = new TimeRange(TimeRange.TYPE_SNAPSHOT, 0, currentManifest.duration * 1000);
notifySeekRangeChanged(seekRange);
} else {
long nowUs = getNowUs();
updateAvailableSegmentBounds(segmentIndex, nowUs);
updateSeekRange(segmentIndex, nowUs);
}
} }
@Override @Override
...@@ -335,7 +336,7 @@ public class DashChunkSource implements ChunkSource { ...@@ -335,7 +336,7 @@ public class DashChunkSource implements ChunkSource {
if (manifestFetcher != null) { if (manifestFetcher != null) {
manifestFetcher.disable(); manifestFetcher.disable();
} }
seekRange = null; availableRange = null;
} }
@Override @Override
...@@ -346,41 +347,8 @@ public class DashChunkSource implements ChunkSource { ...@@ -346,41 +347,8 @@ public class DashChunkSource implements ChunkSource {
MediaPresentationDescription newManifest = manifestFetcher.getManifest(); MediaPresentationDescription newManifest = manifestFetcher.getManifest();
if (currentManifest != newManifest && newManifest != null) { if (currentManifest != newManifest && newManifest != null) {
Representation[] newRepresentations = DashChunkSource.getFilteredRepresentations(newManifest, processManifest(newManifest);
adaptationSetIndex, representationIndices); updateAvailableBounds(getNowUs());
for (Representation representation : newRepresentations) {
RepresentationHolder representationHolder =
representationHolders.get(representation.format.id);
DashSegmentIndex oldIndex = representationHolder.segmentIndex;
int oldIndexLastSegmentNum = oldIndex.getLastSegmentNum();
long oldIndexEndTimeUs = oldIndex.getTimeUs(oldIndexLastSegmentNum)
+ oldIndex.getDurationUs(oldIndexLastSegmentNum);
DashSegmentIndex newIndex = representation.getIndex();
int newIndexFirstSegmentNum = newIndex.getFirstSegmentNum();
long newIndexStartTimeUs = newIndex.getTimeUs(newIndexFirstSegmentNum);
if (oldIndexEndTimeUs < newIndexStartTimeUs) {
// There's a gap between the old manifest and the new one which means we've slipped behind
// the live window and can't proceed.
fatalError = new BehindLiveWindowException();
return;
}
int segmentNumShift;
if (oldIndexEndTimeUs == newIndexStartTimeUs) {
// The new manifest continues where the old one ended, with no overlap.
segmentNumShift = oldIndex.getLastSegmentNum() + 1 - newIndexFirstSegmentNum;
} else {
// The new manifest overlaps with the old one.
segmentNumShift = oldIndex.getSegmentNum(newIndexStartTimeUs) - newIndexFirstSegmentNum;
}
representationHolder.segmentNumShift += segmentNumShift;
representationHolder.segmentIndex = newIndex;
}
currentManifest = newManifest;
finishedCurrentManifest = false;
long nowUs = getNowUs();
updateAvailableSegmentBounds(newRepresentations[0].getIndex(), nowUs);
updateSeekRange(newRepresentations[0].getIndex(), nowUs);
} }
// 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
...@@ -392,8 +360,8 @@ public class DashChunkSource implements ChunkSource { ...@@ -392,8 +360,8 @@ public class DashChunkSource implements ChunkSource {
minUpdatePeriod = 5000; minUpdatePeriod = 5000;
} }
if (finishedCurrentManifest && (android.os.SystemClock.elapsedRealtime() if (android.os.SystemClock.elapsedRealtime()
> manifestFetcher.getManifestLoadStartTimestamp() + minUpdatePeriod)) { > manifestFetcher.getManifestLoadStartTimestamp() + minUpdatePeriod) {
manifestFetcher.requestRefresh(); manifestFetcher.requestRefresh();
} }
} }
...@@ -408,7 +376,14 @@ public class DashChunkSource implements ChunkSource { ...@@ -408,7 +376,14 @@ public class DashChunkSource implements ChunkSource {
evaluation.queueSize = queue.size(); evaluation.queueSize = queue.size();
if (evaluation.format == null || !lastChunkWasInitialization) { if (evaluation.format == null || !lastChunkWasInitialization) {
formatEvaluator.evaluate(queue, playbackPositionUs, formats, evaluation); PeriodHolder periodHolder = null;
if (!queue.isEmpty()) {
periodHolder = periodHolders.get(queue.get(queue.size() - 1).parentId);
}
if (periodHolder == null) {
periodHolder = periodHolders.valueAt(0);
}
formatEvaluator.evaluate(queue, playbackPositionUs, periodHolder.formats, evaluation);
} }
Format selectedFormat = evaluation.format; Format selectedFormat = evaluation.format;
out.queueSize = evaluation.queueSize; out.queueSize = evaluation.queueSize;
...@@ -426,96 +401,122 @@ public class DashChunkSource implements ChunkSource { ...@@ -426,96 +401,122 @@ 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;
RepresentationHolder representationHolder = representationHolders.get(selectedFormat.id); 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;
if (queue.isEmpty()) {
if (currentManifest.dynamic) {
if (startAtLiveEdge) {
// We want live streams to start at the live edge instead of the beginning of the
// manifest
seekPositionUs = Math.max(availableRangeValues[0],
availableRangeValues[1] - liveEdgeLatencyUs);
} else {
seekPositionUs = Math.max(seekPositionUs, availableRangeValues[0]);
// we subtract 1 from the upper bound because it's exclusive for that bound
seekPositionUs = Math.min(seekPositionUs, availableRangeValues[1] - 1);
}
}
periodHolder = findPeriodHolder(seekPositionUs);
segmentStartTimeUs = seekPositionUs;
startingNewPeriod = true;
} else {
if (startAtLiveEdge) {
// now that we know the player is consuming media chunks (since the queue isn't empty),
// set startAtLiveEdge to false so that the user can perform seek operations
startAtLiveEdge = false;
}
MediaChunk previous = queue.get(out.queueSize - 1);
if (previous.isLastChunk) {
// We've reached the end of the stream.
return;
}
segmentNum = previous.chunkIndex + 1;
segmentStartTimeUs = previous.endTimeUs;
if (currentManifest.dynamic) {
if (segmentStartTimeUs < availableRangeValues[0]) {
// This is before the first chunk in the current manifest.
fatalError = new BehindLiveWindowException();
return;
} else if (segmentStartTimeUs >= availableRangeValues[1]) {
// This chunk is beyond the last chunk in the current manifest. If the index is bounded
// we'll need to wait until it's refreshed. If it's unbounded we just need to wait for a
// while before attempting to load the chunk.
return;
}
}
periodHolder = periodHolders.get(previous.parentId);
if (periodHolder == null) {
// the previous chunk was from a period that's no longer on the manifest, therefore the
// next chunk must be the first one in the first period that's still on the manifest
// (note that we can't actually update the segmentNum yet because the new period might
// have a different sequence and it's segmentIndex might not have been loaded yet)
periodHolder = periodHolders.valueAt(0);
startingNewPeriod = true;
} else if (!periodHolder.isIndexUnbounded()
&& segmentStartTimeUs >= periodHolder.getAvailableEndTimeUs()) {
// we reached the end of a period, start the next one (note that we can't actually
// update the segmentNum yet because the new period might have a different
// sequence and it's segmentIndex might not have been loaded yet)
periodHolder = periodHolders.get(previous.parentId + 1);
startingNewPeriod = true;
}
}
RepresentationHolder representationHolder =
periodHolder.representationHolders.get(selectedFormat.id);
Representation selectedRepresentation = representationHolder.representation; Representation selectedRepresentation = representationHolder.representation;
DashSegmentIndex segmentIndex = representationHolder.segmentIndex;
ChunkExtractorWrapper extractorWrapper = representationHolder.extractorWrapper; ChunkExtractorWrapper extractorWrapper = representationHolder.extractorWrapper;
RangedUri pendingInitializationUri = null; RangedUri pendingInitializationUri = null;
RangedUri pendingIndexUri = null; RangedUri pendingIndexUri = null;
if (representationHolder.format == null) { MediaFormat mediaFormat = representationHolder.mediaFormat;
if (mediaFormat == null) {
pendingInitializationUri = selectedRepresentation.getInitializationUri(); pendingInitializationUri = selectedRepresentation.getInitializationUri();
} }
if (segmentIndex == null) { if (representationHolder.segmentIndex == null) {
pendingIndexUri = selectedRepresentation.getIndexUri(); pendingIndexUri = selectedRepresentation.getIndexUri();
} }
if (pendingInitializationUri != null || pendingIndexUri != null) { if (pendingInitializationUri != null || pendingIndexUri != null) {
// We have initialization and/or index requests to make. // We have initialization and/or index requests to make.
Chunk initializationChunk = newInitializationChunk(pendingInitializationUri, pendingIndexUri, Chunk initializationChunk = newInitializationChunk(pendingInitializationUri, pendingIndexUri,
selectedRepresentation, extractorWrapper, dataSource, evaluation.trigger); selectedRepresentation, extractorWrapper, dataSource, periodHolder.manifestIndex,
evaluation.trigger);
lastChunkWasInitialization = true; lastChunkWasInitialization = true;
out.chunk = initializationChunk; out.chunk = initializationChunk;
return; return;
} }
int segmentNum; if (startingNewPeriod) {
boolean indexUnbounded = segmentIndex.getLastSegmentNum() == DashSegmentIndex.INDEX_UNBOUNDED; if (queue.isEmpty()) {
if (indexUnbounded) { // when starting a new period (or beginning playback for the first time), the segment
// Manifests with unbounded indexes aren't updated regularly, so we need to update the // numbering might have been reset, so we'll need to determine the correct number from
// segment bounds before use to ensure that they are accurate to the current time; also if // the representation holder itself
// the bounds have changed, we should update the seek range segmentNum = representationHolder.getSegmentNum(segmentStartTimeUs);
long nowUs = getNowUs(); } else {
int oldFirstAvailableSegmentNum = firstAvailableSegmentNum; segmentNum = representationHolder.getFirstAvailableSegmentNum();
int oldLastAvailableSegmentNum = lastAvailableSegmentNum;
updateAvailableSegmentBounds(segmentIndex, nowUs);
if (oldFirstAvailableSegmentNum != firstAvailableSegmentNum
|| oldLastAvailableSegmentNum != lastAvailableSegmentNum) {
updateSeekRange(segmentIndex, nowUs);
}
}
if (queue.isEmpty()) {
if (currentManifest.dynamic) {
seekRangeValues = seekRange.getCurrentBoundsUs(seekRangeValues);
if (startAtLiveEdge) {
// We want live streams to start at the live edge instead of the beginning of the
// manifest
startAtLiveEdge = false;
seekPositionUs = seekRangeValues[1];
} else {
seekPositionUs = Math.max(seekPositionUs, seekRangeValues[0]);
seekPositionUs = Math.min(seekPositionUs, seekRangeValues[1]);
}
}
segmentNum = segmentIndex.getSegmentNum(seekPositionUs);
// if the index is unbounded then the result of getSegmentNum isn't clamped to ensure that
// it doesn't exceed the last available segment. Clamp it here.
if (indexUnbounded) {
segmentNum = Math.min(segmentNum, lastAvailableSegmentNum);
}
} else {
MediaChunk previous = queue.get(out.queueSize - 1);
segmentNum = previous.isLastChunk ? -1
: previous.chunkIndex + 1 - representationHolder.segmentNumShift;
}
if (currentManifest.dynamic) {
if (segmentNum < firstAvailableSegmentNum) {
// This is before the first chunk in the current manifest.
fatalError = new BehindLiveWindowException();
return;
} else if (segmentNum > lastAvailableSegmentNum) {
// This chunk is beyond the last chunk in the current manifest. If the index is bounded
// we'll need to refresh it. If it's unbounded we just need to wait for a while before
// attempting to load the chunk.
finishedCurrentManifest = !indexUnbounded;
return;
} else if (!indexUnbounded && segmentNum == lastAvailableSegmentNum) {
// This is the last chunk in a dynamic bounded manifest. We'll need to refresh the manifest
// to obtain the next chunk.
finishedCurrentManifest = true;
} }
} }
if (segmentNum == -1) { Chunk nextMediaChunk = newMediaChunk(periodHolder, representationHolder, dataSource,
// We've reached the end of the stream. mediaFormat, segmentNum, evaluation.trigger);
return;
}
Chunk nextMediaChunk = newMediaChunk(representationHolder, dataSource, segmentNum,
evaluation.trigger);
lastChunkWasInitialization = false; lastChunkWasInitialization = false;
out.chunk = nextMediaChunk; out.chunk = nextMediaChunk;
} }
...@@ -534,9 +535,16 @@ public class DashChunkSource implements ChunkSource { ...@@ -534,9 +535,16 @@ public class DashChunkSource implements ChunkSource {
if (chunk instanceof InitializationChunk) { if (chunk instanceof InitializationChunk) {
InitializationChunk initializationChunk = (InitializationChunk) chunk; InitializationChunk initializationChunk = (InitializationChunk) chunk;
String formatId = initializationChunk.format.id; String formatId = initializationChunk.format.id;
RepresentationHolder representationHolder = representationHolders.get(formatId); PeriodHolder periodHolder = periodHolders.get(initializationChunk.parentId);
if (periodHolder == null) {
// period for this initialization chunk may no longer be on the manifest
return;
}
RepresentationHolder representationHolder = periodHolder.representationHolders.get(formatId);
if (initializationChunk.hasFormat()) { if (initializationChunk.hasFormat()) {
representationHolder.format = initializationChunk.getFormat(); representationHolder.mediaFormat = initializationChunk.getFormat();
} }
if (initializationChunk.hasSeekMap()) { if (initializationChunk.hasSeekMap()) {
representationHolder.segmentIndex = new DashWrappingSegmentIndex( representationHolder.segmentIndex = new DashWrappingSegmentIndex(
...@@ -544,6 +552,7 @@ public class DashChunkSource implements ChunkSource { ...@@ -544,6 +552,7 @@ public class DashChunkSource implements ChunkSource {
initializationChunk.dataSpec.uri.toString(), initializationChunk.dataSpec.uri.toString(),
representationHolder.representation.periodStartMs * 1000); representationHolder.representation.periodStartMs * 1000);
} }
// The null check avoids overwriting drmInitData obtained from the manifest with drmInitData // The null check avoids overwriting drmInitData obtained from the manifest with drmInitData
// obtained from the stream, as per DASH IF Interoperability Recommendations V3.0, 7.5.3. // obtained from the stream, as per DASH IF Interoperability Recommendations V3.0, 7.5.3.
if (drmInitData == null && initializationChunk.hasDrmInitData()) { if (drmInitData == null && initializationChunk.hasDrmInitData()) {
...@@ -557,56 +566,42 @@ public class DashChunkSource implements ChunkSource { ...@@ -557,56 +566,42 @@ public class DashChunkSource implements ChunkSource {
// Do nothing. // Do nothing.
} }
private void updateAvailableSegmentBounds(DashSegmentIndex segmentIndex, long nowUs) { private void updateAvailableBounds(long nowUs) {
int indexFirstAvailableSegmentNum = segmentIndex.getFirstSegmentNum(); PeriodHolder firstPeriod = periodHolders.valueAt(0);
int indexLastAvailableSegmentNum = segmentIndex.getLastSegmentNum(); long earliestAvailablePosition = firstPeriod.getAvailableStartTimeUs();
if (indexLastAvailableSegmentNum == DashSegmentIndex.INDEX_UNBOUNDED) { PeriodHolder lastPeriod = periodHolders.valueAt(periodHolders.size() - 1);
// The index is itself unbounded. We need to use the current time to calculate the range of boolean isManifestUnbounded = lastPeriod.isIndexUnbounded();
// available segments. long latestAvailablePosition;
long liveEdgeTimestampUs = nowUs - currentManifest.availabilityStartTime * 1000; if (!currentManifest.dynamic || !isManifestUnbounded) {
if (currentManifest.timeShiftBufferDepth != -1) { latestAvailablePosition = lastPeriod.getAvailableEndTimeUs();
long bufferDepthUs = currentManifest.timeShiftBufferDepth * 1000; } else {
indexFirstAvailableSegmentNum = Math.max(indexFirstAvailableSegmentNum, latestAvailablePosition = TrackRenderer.UNKNOWN_TIME_US;
segmentIndex.getSegmentNum(liveEdgeTimestampUs - bufferDepthUs));
}
// getSegmentNum(liveEdgeTimestampUs) will not be completed yet, so subtract one to get the
// index of the last completed segment.
indexLastAvailableSegmentNum = segmentIndex.getSegmentNum(liveEdgeTimestampUs) - 1;
} }
firstAvailableSegmentNum = indexFirstAvailableSegmentNum;
lastAvailableSegmentNum = indexLastAvailableSegmentNum;
}
private void updateSeekRange(DashSegmentIndex segmentIndex, long nowUs) {
long earliestSeekPosition = segmentIndex.getTimeUs(firstAvailableSegmentNum);
long latestSeekPosition = segmentIndex.getTimeUs(lastAvailableSegmentNum)
+ segmentIndex.getDurationUs(lastAvailableSegmentNum);
if (currentManifest.dynamic) { if (currentManifest.dynamic) {
long liveEdgeTimestampUs; if (isManifestUnbounded) {
if (segmentIndex.getLastSegmentNum() == DashSegmentIndex.INDEX_UNBOUNDED) { latestAvailablePosition = nowUs - currentManifest.availabilityStartTime * 1000;
liveEdgeTimestampUs = nowUs - currentManifest.availabilityStartTime * 1000; } else if (!lastPeriod.isIndexExplicit()) {
} else { // Some segments defined by the index may not be available yet. Bound the calculated live
liveEdgeTimestampUs = segmentIndex.getTimeUs(segmentIndex.getLastSegmentNum()) // edge based on the elapsed time since the manifest became available.
+ segmentIndex.getDurationUs(segmentIndex.getLastSegmentNum()); latestAvailablePosition = Math.min(latestAvailablePosition,
if (!segmentIndex.isExplicit()) { nowUs - currentManifest.availabilityStartTime * 1000);
// 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.
liveEdgeTimestampUs = Math.min(liveEdgeTimestampUs,
nowUs - currentManifest.availabilityStartTime * 1000);
}
} }
// it's possible that the live edge latency actually puts our latest position before // if we have a limited timeshift buffer, we need to adjust the earliest seek position so
// the earliest position in the case of a DVR-like stream that's just starting up, so // that it doesn't start before the buffer
// in that case just return the earliest position instead if (currentManifest.timeShiftBufferDepth != -1) {
latestSeekPosition = Math.max(earliestSeekPosition, liveEdgeTimestampUs - liveEdgeLatencyUs); long bufferDepthUs = currentManifest.timeShiftBufferDepth * 1000;
earliestAvailablePosition = Math.max(earliestAvailablePosition,
latestAvailablePosition - bufferDepthUs);
}
} }
TimeRange newSeekRange = new TimeRange(TimeRange.TYPE_SNAPSHOT, earliestSeekPosition, TimeRange newAvailableRange = new TimeRange(TimeRange.TYPE_SNAPSHOT, earliestAvailablePosition,
latestSeekPosition); latestAvailablePosition);
if (seekRange == null || !seekRange.equals(newSeekRange)) { if (availableRange == null || !availableRange.equals(newAvailableRange)) {
seekRange = newSeekRange; availableRange = newAvailableRange;
notifySeekRangeChanged(seekRange); notifyAvailableRangeChanged(availableRange);
} }
} }
...@@ -616,7 +611,7 @@ public class DashChunkSource implements ChunkSource { ...@@ -616,7 +611,7 @@ public class DashChunkSource implements ChunkSource {
private Chunk newInitializationChunk(RangedUri initializationUri, RangedUri indexUri, private Chunk newInitializationChunk(RangedUri initializationUri, RangedUri indexUri,
Representation representation, ChunkExtractorWrapper extractor, DataSource dataSource, Representation representation, ChunkExtractorWrapper extractor, DataSource dataSource,
int trigger) { int manifestIndex, int trigger) {
RangedUri requestUri; RangedUri requestUri;
if (initializationUri != null) { if (initializationUri != null) {
// It's common for initialization and index data to be stored adjacently. Attempt to merge // It's common for initialization and index data to be stored adjacently. Attempt to merge
...@@ -630,37 +625,36 @@ public class DashChunkSource implements ChunkSource { ...@@ -630,37 +625,36 @@ public class DashChunkSource implements ChunkSource {
} }
DataSpec dataSpec = new DataSpec(requestUri.getUri(), requestUri.start, requestUri.length, DataSpec dataSpec = new DataSpec(requestUri.getUri(), requestUri.start, requestUri.length,
representation.getCacheKey()); representation.getCacheKey());
return new InitializationChunk(dataSource, dataSpec, trigger, representation.format, extractor); return new InitializationChunk(dataSource, dataSpec, trigger, representation.format,
extractor, manifestIndex);
} }
private Chunk newMediaChunk(RepresentationHolder representationHolder, DataSource dataSource, private Chunk newMediaChunk(PeriodHolder periodHolder, RepresentationHolder representationHolder,
int segmentNum, int trigger) { DataSource dataSource, MediaFormat mediaFormat, int segmentNum, int trigger) {
Representation representation = representationHolder.representation; Representation representation = representationHolder.representation;
DashSegmentIndex segmentIndex = representationHolder.segmentIndex;
long startTimeUs = segmentIndex.getTimeUs(segmentNum);
long endTimeUs = startTimeUs + segmentIndex.getDurationUs(segmentNum);
int absoluteSegmentNum = segmentNum + representationHolder.segmentNumShift; long startTimeUs = representationHolder.getSegmentStartTimeUs(segmentNum);
long endTimeUs = representationHolder.getSegmentEndTimeUs(segmentNum);
boolean isLastSegment = !currentManifest.dynamic boolean isLastSegment = !currentManifest.dynamic
&& segmentNum == segmentIndex.getLastSegmentNum(); && periodHolders.valueAt(periodHolders.size() - 1) == periodHolder
&& representationHolder.isLastSegment(segmentNum);
RangedUri segmentUri = segmentIndex.getSegmentUrl(segmentNum); RangedUri segmentUri = representationHolder.getSegmentUrl(segmentNum);
DataSpec dataSpec = new DataSpec(segmentUri.getUri(), segmentUri.start, segmentUri.length, DataSpec dataSpec = new DataSpec(segmentUri.getUri(), segmentUri.start, segmentUri.length,
representation.getCacheKey()); representation.getCacheKey());
long sampleOffsetUs = representation.periodStartMs * 1000 long sampleOffsetUs = periodHolder.startTimeUs - representation.presentationTimeOffsetUs;
- representation.presentationTimeOffsetUs;
if (representation.format.mimeType.equals(MimeTypes.TEXT_VTT)) { if (representation.format.mimeType.equals(MimeTypes.TEXT_VTT)) {
MediaFormat mediaFormat = MediaFormat.createTextFormat(MimeTypes.TEXT_VTT,
MediaFormat.NO_VALUE, representation.format.language);
return new SingleSampleMediaChunk(dataSource, dataSpec, Chunk.TRIGGER_INITIAL, return new SingleSampleMediaChunk(dataSource, dataSpec, Chunk.TRIGGER_INITIAL,
representation.format, startTimeUs, endTimeUs, absoluteSegmentNum, isLastSegment, representation.format, startTimeUs, endTimeUs, segmentNum, isLastSegment,
mediaFormat, null); MediaFormat.createTextFormat(MimeTypes.TEXT_VTT, MediaFormat.NO_VALUE,
representation.format.language), null, periodHolder.manifestIndex);
} else { } else {
boolean isMediaFormatFinal = (mediaFormat != null);
return new ContainerMediaChunk(dataSource, dataSpec, trigger, representation.format, return new ContainerMediaChunk(dataSource, dataSpec, trigger, representation.format,
startTimeUs, endTimeUs, absoluteSegmentNum, isLastSegment, sampleOffsetUs, startTimeUs, endTimeUs, segmentNum, isLastSegment, sampleOffsetUs,
representationHolder.extractorWrapper, representationHolder.format, drmInitData, true); representationHolder.extractorWrapper, mediaFormat, drmInitData, isMediaFormatFinal,
periodHolder.manifestIndex);
} }
} }
...@@ -682,23 +676,6 @@ public class DashChunkSource implements ChunkSource { ...@@ -682,23 +676,6 @@ public class DashChunkSource implements ChunkSource {
return mimeType; return mimeType;
} }
private static Representation[] getFilteredRepresentations(MediaPresentationDescription manifest,
int adaptationSetIndex, int[] representationIndices) {
AdaptationSet adaptationSet = manifest.periods.get(0).adaptationSets.get(adaptationSetIndex);
List<Representation> representations = adaptationSet.representations;
if (representationIndices == null) {
Representation[] filteredRepresentations = new Representation[representations.size()];
representations.toArray(filteredRepresentations);
return filteredRepresentations;
} else {
Representation[] filteredRepresentations = new Representation[representationIndices.length];
for (int i = 0; i < representationIndices.length; i++) {
filteredRepresentations[i] = representations.get(representationIndices[i]);
}
return filteredRepresentations;
}
}
private static DrmInitData getDrmInitData(MediaPresentationDescription manifest, private static DrmInitData getDrmInitData(MediaPresentationDescription manifest,
int adaptationSetIndex) { int adaptationSetIndex) {
AdaptationSet adaptationSet = manifest.periods.get(0).adaptationSets.get(adaptationSetIndex); AdaptationSet adaptationSet = manifest.periods.get(0).adaptationSets.get(adaptationSetIndex);
...@@ -708,7 +685,8 @@ public class DashChunkSource implements ChunkSource { ...@@ -708,7 +685,8 @@ public class DashChunkSource implements ChunkSource {
return null; return null;
} else { } else {
DrmInitData.Mapped drmInitData = null; DrmInitData.Mapped drmInitData = null;
for (ContentProtection contentProtection : adaptationSet.contentProtections) { for (int i = 0; i < adaptationSet.contentProtections.size(); i++) {
ContentProtection contentProtection = adaptationSet.contentProtections.get(i);
if (contentProtection.uuid != null && contentProtection.data != null) { if (contentProtection.uuid != null && contentProtection.data != null) {
if (drmInitData == null) { if (drmInitData == null) {
drmInitData = new DrmInitData.Mapped(drmInitMimeType); drmInitData = new DrmInitData.Mapped(drmInitMimeType);
...@@ -730,26 +708,96 @@ public class DashChunkSource implements ChunkSource { ...@@ -730,26 +708,96 @@ public class DashChunkSource implements ChunkSource {
Collections.singletonList(period)); Collections.singletonList(period));
} }
private void notifySeekRangeChanged(final TimeRange seekRange) { private PeriodHolder findPeriodHolder(long positionUs) {
// if positionUs is before the first period, return the first period
if (positionUs < periodHolders.valueAt(0).getAvailableStartTimeUs()) {
return periodHolders.valueAt(0);
}
for (int i = 0; i < periodHolders.size(); i++) {
PeriodHolder periodHolder = periodHolders.valueAt(i);
if (positionUs >= periodHolder.getAvailableStartTimeUs()
&& (periodHolder.isIndexUnbounded()
|| positionUs < periodHolder.getAvailableEndTimeUs())) {
return periodHolder;
}
}
// if positionUs is after the last period, return the last period
return periodHolders.valueAt(periodHolders.size() - 1);
}
private void processManifest(MediaPresentationDescription manifest) {
Period firstPeriod = manifest.periods.get(0);
while (periodHolders.size() > 0
&& periodHolders.valueAt(0).startTimeUs < firstPeriod.startMs * 1000) {
PeriodHolder periodHolder = periodHolders.valueAt(0);
// TODO: a better call would be periodHolders.removeAt(0), but that was added in
// API 11 and this project currently uses API 9; if that changes, we should switch
// this to removeAt(0);
periodHolders.remove(periodHolder.manifestIndex);
}
int periodIndex = 0;
for (int i = 0; i < manifest.periods.size(); i++) {
Period period = manifest.periods.get(i);
AdaptationSet adaptationSet = period.adaptationSets.get(adaptationSetIndex);
List<Representation> representations = adaptationSet.representations;
Representation newRepresentations[];
if (representationIndices == null) {
newRepresentations = new Representation[representations.size()];
representations.toArray(newRepresentations);
} else {
newRepresentations = new Representation[representationIndices.length];
for (int j = 0; j < representationIndices.length; j++) {
newRepresentations[j] = representations.get(representationIndices[j]);
}
}
PeriodHolder periodHolder = periodHolders.valueAt(periodIndex);
if (periodHolder == null) {
long periodStartUs = period.startMs * 1000;
periodHolder = new PeriodHolder(periodHolderNextIndex, periodStartUs, newRepresentations);
periodHolders.put(periodHolderNextIndex, periodHolder);
periodHolderNextIndex++;
} else {
for (int j = 0; j < newRepresentations.length; j++) {
RepresentationHolder representationHolder =
periodHolder.representationHolders.get(newRepresentations[j].format.id);
try {
representationHolder.updateRepresentation(newRepresentations[j]);
} catch (BehindLiveWindowException e) {
fatalError = e;
return;
}
}
}
periodIndex++;
}
currentManifest = manifest;
}
private void notifyAvailableRangeChanged(final TimeRange seekRange) {
if (eventHandler != null && eventListener != null) { if (eventHandler != null && eventListener != null) {
eventHandler.post(new Runnable() { eventHandler.post(new Runnable() {
@Override @Override
public void run() { public void run() {
eventListener.onSeekRangeChanged(seekRange); eventListener.onAvailableRangeChanged(seekRange);
} }
}); });
} }
} }
private static class RepresentationHolder { private static final class RepresentationHolder {
public final Representation representation;
public final ChunkExtractorWrapper extractorWrapper; public final ChunkExtractorWrapper extractorWrapper;
public Representation representation;
public DashSegmentIndex segmentIndex; public DashSegmentIndex segmentIndex;
public MediaFormat format; public MediaFormat mediaFormat;
public int segmentNumShift; private int segmentNumShift;
public RepresentationHolder(Representation representation, public RepresentationHolder(Representation representation,
ChunkExtractorWrapper extractorWrapper) { ChunkExtractorWrapper extractorWrapper) {
...@@ -758,6 +806,173 @@ public class DashChunkSource implements ChunkSource { ...@@ -758,6 +806,173 @@ public class DashChunkSource implements ChunkSource {
this.segmentIndex = representation.getIndex(); this.segmentIndex = representation.getIndex();
} }
public void updateRepresentation(Representation newRepresentation)
throws BehindLiveWindowException{
DashSegmentIndex oldIndex = segmentIndex;
int oldIndexLastSegmentNum = oldIndex.getLastSegmentNum();
long oldIndexEndTimeUs = oldIndex.getTimeUs(oldIndexLastSegmentNum)
+ oldIndex.getDurationUs(oldIndexLastSegmentNum);
DashSegmentIndex newIndex = newRepresentation.getIndex();
int newIndexFirstSegmentNum = newIndex.getFirstSegmentNum();
long newIndexStartTimeUs = newIndex.getTimeUs(newIndexFirstSegmentNum);
int segmentNumShift;
if (oldIndexEndTimeUs == newIndexStartTimeUs) {
// The new manifest continues where the old one ended, with no overlap.
segmentNumShift = oldIndex.getLastSegmentNum() + 1 - newIndexFirstSegmentNum;
} else if (oldIndexEndTimeUs < newIndexStartTimeUs) {
// There's a gap between the old manifest and the new one which means we've slipped
// behind the live window and can't proceed.
throw new BehindLiveWindowException();
} else {
// The new manifest overlaps with the old one.
segmentNumShift = oldIndex.getSegmentNum(newIndexStartTimeUs) - newIndexFirstSegmentNum;
}
representation = newRepresentation;
segmentIndex = newIndex;
this.segmentNumShift += segmentNumShift;
}
public int getSegmentNum(long positionUs) {
return segmentIndex.getSegmentNum(positionUs) + segmentNumShift;
}
public long getSegmentStartTimeUs(int segmentNum) {
return segmentIndex.getTimeUs(segmentNum - segmentNumShift);
}
public long getSegmentEndTimeUs(int segmentNum) {
return getSegmentStartTimeUs(segmentNum)
+ segmentIndex.getDurationUs(segmentNum - segmentNumShift);
}
public boolean isLastSegment(int segmentNum) {
return (segmentNum - segmentNumShift) == segmentIndex.getLastSegmentNum();
}
public int getFirstAvailableSegmentNum() {
return segmentIndex.getFirstSegmentNum() + segmentNumShift;
}
public int getLastAvailableSegmentNum() {
int lastSegmentNum = segmentIndex.getLastSegmentNum();
if (lastSegmentNum == DashSegmentIndex.INDEX_UNBOUNDED) {
return DashSegmentIndex.INDEX_UNBOUNDED;
} else {
return lastSegmentNum + segmentNumShift;
}
}
public RangedUri getSegmentUrl(int segmentNum) {
return segmentIndex.getSegmentUrl(segmentNum - segmentNumShift);
}
}
private static final class PeriodHolder {
public final int manifestIndex;
public final long startTimeUs;
public final long durationUs;
public final String mimeType;
public final Format[] formats;
public final HashMap<String, RepresentationHolder> representationHolders;
private final int maxWidth;
private final int maxHeight;
public PeriodHolder(int manifestIndex, long startTimeUs, Representation[] representations) {
this.manifestIndex = manifestIndex;
this.startTimeUs = startTimeUs;
this.formats = new Format[representations.length];
this.representationHolders = new HashMap<>();
int maxWidth = 0;
int maxHeight = 0;
String mimeType = "";
for (int i = 0; i < representations.length; i++) {
Representation representation = representations[i];
formats[i] = representation.format;
mimeType = getMediaMimeType(representation);
maxWidth = Math.max(formats[i].width, maxWidth);
maxHeight = Math.max(formats[i].height, maxHeight);
Extractor extractor = mimeTypeIsWebm(formats[i].mimeType) ? new WebmExtractor()
: new FragmentedMp4Extractor();
RepresentationHolder representationHolder =
new RepresentationHolder(representation, new ChunkExtractorWrapper(extractor));
representationHolders.put(formats[i].id, representationHolder);
}
this.maxWidth = maxWidth;
this.maxHeight = maxHeight;
this.mimeType = mimeType;
long durationMs =
representationHolders.get(formats[0].id).representation.periodDurationMs;
if (durationMs == TrackRenderer.UNKNOWN_TIME_US) {
durationUs = TrackRenderer.UNKNOWN_TIME_US;
} else {
durationUs = durationMs * 1000;
}
Arrays.sort(formats, new DecreasingBandwidthComparator());
}
public long getAvailableStartTimeUs() {
RepresentationHolder representationHolder = representationHolders.get(formats[0].id);
// in this case, we only want to use the segment index if it was defined in the manifest,
// otherwise we should just base this on the period information that was in the manifest
DashSegmentIndex segmentIndex = representationHolder.representation.getIndex();
if (segmentIndex != null) {
return segmentIndex.getTimeUs(segmentIndex.getFirstSegmentNum());
} else {
return startTimeUs;
}
}
public long getAvailableEndTimeUs() {
RepresentationHolder representationHolder = representationHolders.get(formats[0].id);
// in this case, we only want to use the segment index if it was defined in the manifest,
// otherwise we should just base this on the period information that was in the manifest
DashSegmentIndex segmentIndex = representationHolder.representation.getIndex();
if (segmentIndex != null) {
int lastSegmentNum = segmentIndex.getLastSegmentNum();
if (lastSegmentNum == DashSegmentIndex.INDEX_UNBOUNDED) {
throw new IllegalStateException("Can't call this method on a period with and unbounded "
+ "index");
}
return segmentIndex.getTimeUs(lastSegmentNum) + segmentIndex.getDurationUs(lastSegmentNum);
} else {
return startTimeUs + (representationHolder.representation.periodDurationMs * 1000);
}
}
public boolean isIndexUnbounded() {
RepresentationHolder representationHolder = representationHolders.get(formats[0].id);
// in this case, we only want to use the segment index if it was defined in the manifest,
// otherwise we should just base this on the period information that was in the manifest
DashSegmentIndex segmentIndex = representationHolder.representation.getIndex();
if (segmentIndex != null) {
int lastSegmentNum = segmentIndex.getLastSegmentNum();
if (lastSegmentNum == DashSegmentIndex.INDEX_UNBOUNDED) {
return lastSegmentNum == DashSegmentIndex.INDEX_UNBOUNDED;
}
}
return false;
}
public boolean isIndexExplicit() {
RepresentationHolder representationHolder = representationHolders.get(formats[0].id);
// in this case, we only want to use the segment index if it was defined in the manifest,
// otherwise we should just base this on the period information that was in the manifest
DashSegmentIndex segmentIndex = representationHolder.representation.getIndex();
if (segmentIndex != null) {
return segmentIndex.isExplicit();
}
return true;
}
} }
} }
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