Implemented limited support for multi-period DASH manifests.

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

GitHub Issue: #557
This commit is contained in:
Oliver Woodman 2015-09-01 13:54:00 +01:00
parent f69f948991
commit 0efaec59b8
10 changed files with 912 additions and 365 deletions

View file

@ -47,7 +47,7 @@ public class EventLogger implements DemoPlayer.Listener, DemoPlayer.InfoListener
private long sessionStartTimeMs;
private long[] loadStartTimeMs;
private long[] seekRangeValuesUs;
private long[] availableRangeValuesUs;
public EventLogger() {
loadStartTimeMs = new long[DemoPlayer.RENDERER_COUNT];
@ -171,10 +171,10 @@ public class EventLogger implements DemoPlayer.Listener, DemoPlayer.InfoListener
}
@Override
public void onSeekRangeChanged(TimeRange seekRange) {
seekRangeValuesUs = seekRange.getCurrentBoundsUs(seekRangeValuesUs);
Log.d(TAG, "seekRange [ " + seekRange.type + ", " + seekRangeValuesUs[0] + ", "
+ seekRangeValuesUs[1] + "]");
public void onAvailableRangeChanged(TimeRange availableRange) {
availableRangeValuesUs = availableRange.getCurrentBoundsUs(availableRangeValuesUs);
Log.d(TAG, "availableRange [ " + availableRange.type + ", " + availableRangeValuesUs[0] + ", "
+ availableRangeValuesUs[1] + "]");
}
private void printInternalError(String type, Exception e) {

View file

@ -124,7 +124,7 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi
int mediaStartTimeMs, int mediaEndTimeMs, long elapsedRealtimeMs, long loadDurationMs);
void onDecoderInitialized(String decoderName, long elapsedRealtimeMs,
long initializationDurationMs);
void onSeekRangeChanged(TimeRange seekRange);
void onAvailableRangeChanged(TimeRange availableRange);
}
/**
@ -509,9 +509,9 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi
}
@Override
public void onSeekRangeChanged(TimeRange seekRange) {
public void onAvailableRangeChanged(TimeRange availableRange) {
if (infoListener != null) {
infoListener.onSeekRangeChanged(seekRange);
infoListener.onAvailableRangeChanged(availableRange);
}
}

View file

@ -25,6 +25,7 @@ import com.google.android.exoplayer.chunk.ChunkOperationHolder;
import com.google.android.exoplayer.chunk.Format;
import com.google.android.exoplayer.chunk.FormatEvaluator;
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.dash.mpd.AdaptationSet;
import com.google.android.exoplayer.dash.mpd.MediaPresentationDescription;
@ -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_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_REALTIME_OFFSET_MS = 1000;
private static final long AVAILABILITY_CURRENT_TIME_MS =
@ -98,19 +104,96 @@ public class DashChunkSourceTest extends InstrumentationTestCase {
assertEquals(TALL_HEIGHT, format.maxHeight);
}
public void testGetSeekRangeOnVod() {
public void testGetAvailableRangeOnVod() {
DashChunkSource chunkSource = new DashChunkSource(generateVodMpd(), AdaptationSet.TYPE_VIDEO,
null, null, mock(FormatEvaluator.class));
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(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() {
SingleSegmentBase segmentBase1 = new SingleSegmentBase("https://example.com/1.mp4");
Representation representation1 =
@ -131,192 +214,197 @@ public class DashChunkSourceTest extends InstrumentationTestCase {
public void testLiveEdgeNoLatency() {
long startTimeMs = 0;
long liveEdgeLatencyMs = 0;
long seekPositionMs = LIVE_SEEK_BEYOND_EDGE_MS;
long seekRangeStartMs = 0;
long seekRangeEndMs = LIVE_DURATION_MS - liveEdgeLatencyMs;
long seekPositionMs = startTimeMs + LIVE_DURATION_MS - liveEdgeLatencyMs;
long availableRangeStartMs = 0;
long availableRangeEndMs = LIVE_DURATION_MS;
long chunkStartTimeMs = 4000;
long chunkEndTimeMs = 5000;
checkLiveTimelineConsistency(startTimeMs, liveEdgeLatencyMs, seekPositionMs,
seekRangeStartMs, seekRangeEndMs, chunkStartTimeMs, chunkEndTimeMs);
availableRangeStartMs, availableRangeEndMs, chunkStartTimeMs, chunkEndTimeMs);
}
public void testLiveEdgeAlmostNoLatency() {
long startTimeMs = 0;
long liveEdgeLatencyMs = 1;
long seekPositionMs = LIVE_SEEK_BEYOND_EDGE_MS;
long seekRangeStartMs = 0;
long seekRangeEndMs = LIVE_DURATION_MS - liveEdgeLatencyMs;
long seekPositionMs = startTimeMs + LIVE_DURATION_MS - liveEdgeLatencyMs;
long availableRangeStartMs = 0;
long availableRangeEndMs = LIVE_DURATION_MS;
long chunkStartTimeMs = 4000;
long chunkEndTimeMs = 5000;
checkLiveTimelineConsistency(startTimeMs, liveEdgeLatencyMs, seekPositionMs,
seekRangeStartMs, seekRangeEndMs, chunkStartTimeMs, chunkEndTimeMs);
availableRangeStartMs, availableRangeEndMs, chunkStartTimeMs, chunkEndTimeMs);
}
public void testLiveEdge500msLatency() {
long startTimeMs = 0;
long liveEdgeLatencyMs = 500;
long seekPositionMs = LIVE_SEEK_BEYOND_EDGE_MS;
long seekRangeStartMs = 0;
long seekRangeEndMs = LIVE_DURATION_MS - liveEdgeLatencyMs;
long seekPositionMs = startTimeMs + LIVE_DURATION_MS - liveEdgeLatencyMs;
long availableRangeStartMs = 0;
long availableRangeEndMs = LIVE_DURATION_MS;
long chunkStartTimeMs = 4000;
long chunkEndTimeMs = 5000;
checkLiveTimelineConsistency(startTimeMs, liveEdgeLatencyMs, seekPositionMs,
seekRangeStartMs, seekRangeEndMs, chunkStartTimeMs, chunkEndTimeMs);
availableRangeStartMs, availableRangeEndMs, chunkStartTimeMs, chunkEndTimeMs);
}
public void testLiveEdge1000msLatency() {
long startTimeMs = 0;
long liveEdgeLatencyMs = 1000;
long seekPositionMs = LIVE_SEEK_BEYOND_EDGE_MS;
long seekRangeStartMs = 0;
long seekRangeEndMs = LIVE_DURATION_MS - liveEdgeLatencyMs;
long seekPositionMs = startTimeMs + LIVE_DURATION_MS - liveEdgeLatencyMs;
long availableRangeStartMs = 0;
long availableRangeEndMs = LIVE_DURATION_MS;
long chunkStartTimeMs = 4000;
long chunkEndTimeMs = 5000;
checkLiveTimelineConsistency(startTimeMs, liveEdgeLatencyMs, seekPositionMs,
seekRangeStartMs, seekRangeEndMs, chunkStartTimeMs, chunkEndTimeMs);
availableRangeStartMs, availableRangeEndMs, chunkStartTimeMs, chunkEndTimeMs);
}
public void testLiveEdge1001msLatency() {
long startTimeMs = 0;
long liveEdgeLatencyMs = 1001;
long seekPositionMs = LIVE_SEEK_BEYOND_EDGE_MS;
long seekRangeStartMs = 0;
long seekRangeEndMs = LIVE_DURATION_MS - liveEdgeLatencyMs;
long seekPositionMs = startTimeMs + LIVE_DURATION_MS - liveEdgeLatencyMs;
long availableRangeStartMs = 0;
long availableRangeEndMs = LIVE_DURATION_MS;
long chunkStartTimeMs = 3000;
long chunkEndTimeMs = 4000;
checkLiveTimelineConsistency(startTimeMs, liveEdgeLatencyMs, seekPositionMs,
seekRangeStartMs, seekRangeEndMs, chunkStartTimeMs, chunkEndTimeMs);
availableRangeStartMs, availableRangeEndMs, chunkStartTimeMs, chunkEndTimeMs);
}
public void testLiveEdge2500msLatency() {
long startTimeMs = 0;
long liveEdgeLatencyMs = 2500;
long seekPositionMs = LIVE_SEEK_BEYOND_EDGE_MS;
long seekRangeStartMs = 0;
long seekRangeEndMs = LIVE_DURATION_MS - liveEdgeLatencyMs;
long seekPositionMs = startTimeMs + LIVE_DURATION_MS - liveEdgeLatencyMs;
long availableRangeStartMs = 0;
long availableRangeEndMs = LIVE_DURATION_MS;
long chunkStartTimeMs = 2000;
long chunkEndTimeMs = 3000;
checkLiveTimelineConsistency(startTimeMs, liveEdgeLatencyMs, seekPositionMs,
seekRangeStartMs, seekRangeEndMs, chunkStartTimeMs, chunkEndTimeMs);
availableRangeStartMs, availableRangeEndMs, chunkStartTimeMs, chunkEndTimeMs);
}
public void testLiveEdgeVeryHighLatency() {
long startTimeMs = 0;
long liveEdgeLatencyMs = 10000;
long seekPositionMs = LIVE_SEEK_BEYOND_EDGE_MS;
long seekRangeStartMs = 0;
long seekRangeEndMs = 0;
long seekPositionMs = startTimeMs + LIVE_DURATION_MS - liveEdgeLatencyMs;
long availableRangeStartMs = 0;
long availableRangeEndMs = LIVE_DURATION_MS;
long chunkStartTimeMs = 0;
long chunkEndTimeMs = 1000;
checkLiveTimelineConsistency(startTimeMs, liveEdgeLatencyMs, seekPositionMs,
seekRangeStartMs, seekRangeEndMs, chunkStartTimeMs, chunkEndTimeMs);
availableRangeStartMs, availableRangeEndMs, chunkStartTimeMs, chunkEndTimeMs);
}
public void testLiveEdgeNoLatencyInProgress() {
long startTimeMs = 3000;
long liveEdgeLatencyMs = 0;
long seekPositionMs = LIVE_SEEK_BEYOND_EDGE_MS;
long seekRangeStartMs = 3000;
long seekRangeEndMs = 3000 + LIVE_DURATION_MS - liveEdgeLatencyMs;
long seekPositionMs = startTimeMs + LIVE_DURATION_MS - liveEdgeLatencyMs;
long availableRangeStartMs = 3000;
long availableRangeEndMs = 3000 + LIVE_DURATION_MS;
long chunkStartTimeMs = 7000;
long chunkEndTimeMs = 8000;
checkLiveTimelineConsistency(startTimeMs, liveEdgeLatencyMs, seekPositionMs,
seekRangeStartMs, seekRangeEndMs, chunkStartTimeMs, chunkEndTimeMs);
availableRangeStartMs, availableRangeEndMs, chunkStartTimeMs, chunkEndTimeMs);
}
public void testLiveEdgeAlmostNoLatencyInProgress() {
long startTimeMs = 3000;
long liveEdgeLatencyMs = 1;
long seekPositionMs = LIVE_SEEK_BEYOND_EDGE_MS;
long seekRangeStartMs = 3000;
long seekRangeEndMs = 3000 + LIVE_DURATION_MS - liveEdgeLatencyMs;
long seekPositionMs = startTimeMs + LIVE_DURATION_MS - liveEdgeLatencyMs;
long availableRangeStartMs = 3000;
long availableRangeEndMs = 3000 + LIVE_DURATION_MS;
long chunkStartTimeMs = 7000;
long chunkEndTimeMs = 8000;
checkLiveTimelineConsistency(startTimeMs, liveEdgeLatencyMs, seekPositionMs,
seekRangeStartMs, seekRangeEndMs, chunkStartTimeMs, chunkEndTimeMs);
availableRangeStartMs, availableRangeEndMs, chunkStartTimeMs, chunkEndTimeMs);
}
public void testLiveEdge500msLatencyInProgress() {
long startTimeMs = 3000;
long liveEdgeLatencyMs = 500;
long seekPositionMs = LIVE_SEEK_BEYOND_EDGE_MS;
long seekRangeStartMs = 3000;
long seekRangeEndMs = 3000 + LIVE_DURATION_MS - liveEdgeLatencyMs;
long seekPositionMs = startTimeMs + LIVE_DURATION_MS - liveEdgeLatencyMs;
long availableRangeStartMs = 3000;
long availableRangeEndMs = 3000 + LIVE_DURATION_MS;
long chunkStartTimeMs = 7000;
long chunkEndTimeMs = 8000;
checkLiveTimelineConsistency(startTimeMs, liveEdgeLatencyMs, seekPositionMs,
seekRangeStartMs, seekRangeEndMs, chunkStartTimeMs, chunkEndTimeMs);
availableRangeStartMs, availableRangeEndMs, chunkStartTimeMs, chunkEndTimeMs);
}
public void testLiveEdge1000msLatencyInProgress() {
long startTimeMs = 3000;
long liveEdgeLatencyMs = 1000;
long seekPositionMs = LIVE_SEEK_BEYOND_EDGE_MS;
long seekRangeStartMs = 3000;
long seekRangeEndMs = 3000 + LIVE_DURATION_MS - liveEdgeLatencyMs;
long seekPositionMs = startTimeMs + LIVE_DURATION_MS - liveEdgeLatencyMs;
long availableRangeStartMs = 3000;
long availableRangeEndMs = 3000 + LIVE_DURATION_MS;
long chunkStartTimeMs = 7000;
long chunkEndTimeMs = 8000;
checkLiveTimelineConsistency(startTimeMs, liveEdgeLatencyMs, seekPositionMs,
seekRangeStartMs, seekRangeEndMs, chunkStartTimeMs, chunkEndTimeMs);
availableRangeStartMs, availableRangeEndMs, chunkStartTimeMs, chunkEndTimeMs);
}
public void testLiveEdge1001msLatencyInProgress() {
long startTimeMs = 3000;
long liveEdgeLatencyMs = 1001;
long seekPositionMs = LIVE_SEEK_BEYOND_EDGE_MS;
long seekRangeStartMs = 3000;
long seekRangeEndMs = 3000 + LIVE_DURATION_MS - liveEdgeLatencyMs;
long seekPositionMs = startTimeMs + LIVE_DURATION_MS - liveEdgeLatencyMs;
long availableRangeStartMs = 3000;
long availableRangeEndMs = 3000 + LIVE_DURATION_MS;
long chunkStartTimeMs = 6000;
long chunkEndTimeMs = 7000;
checkLiveTimelineConsistency(startTimeMs, liveEdgeLatencyMs, seekPositionMs,
seekRangeStartMs, seekRangeEndMs, chunkStartTimeMs, chunkEndTimeMs);
availableRangeStartMs, availableRangeEndMs, chunkStartTimeMs, chunkEndTimeMs);
}
public void testLiveEdge2500msLatencyInProgress() {
long startTimeMs = 3000;
long liveEdgeLatencyMs = 2500;
long seekPositionMs = LIVE_SEEK_BEYOND_EDGE_MS;
long seekRangeStartMs = 3000;
long seekRangeEndMs = 3000 + LIVE_DURATION_MS - liveEdgeLatencyMs;
long seekPositionMs = startTimeMs + LIVE_DURATION_MS - liveEdgeLatencyMs;
long availableRangeStartMs = 3000;
long availableRangeEndMs = 3000 + LIVE_DURATION_MS;
long chunkStartTimeMs = 5000;
long chunkEndTimeMs = 6000;
checkLiveTimelineConsistency(startTimeMs, liveEdgeLatencyMs, seekPositionMs,
seekRangeStartMs, seekRangeEndMs, chunkStartTimeMs, chunkEndTimeMs);
availableRangeStartMs, availableRangeEndMs, chunkStartTimeMs, chunkEndTimeMs);
}
public void testLiveEdgeVeryHighLatencyInProgress() {
long startTimeMs = 3000;
long liveEdgeLatencyMs = 10000;
long seekPositionMs = LIVE_SEEK_BEYOND_EDGE_MS;
long seekRangeStartMs = 3000;
long seekRangeEndMs = 3000;
long seekPositionMs = startTimeMs + LIVE_DURATION_MS - liveEdgeLatencyMs;
long availableRangeStartMs = 3000;
long availableRangeEndMs = 3000 + LIVE_DURATION_MS;
long chunkStartTimeMs = 3000;
long chunkEndTimeMs = 4000;
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,
seekPositionMs, 0, 0, 1000);
0, availableRangeEndMs, 0, 1000);
checkLiveEdgeLatencyWithTemplateAndLimitedTimeshift(startTimeMs, liveEdgeLatencyMs,
seekPositionMs, seekRangeStartMs, seekRangeEndMs, chunkStartTimeMs, chunkEndTimeMs);
seekPositionMs, availableRangeStartMs, availableRangeEndMs, chunkStartTimeMs,
chunkEndTimeMs);
}
private static Representation generateVodRepresentation(long startTimeMs, long duration,
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);
}
@ -341,6 +429,18 @@ public class DashChunkSourceTest extends InstrumentationTestCase {
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,
List<Representation> representations, boolean limitTimeshiftBuffer) {
Representation firstRepresentation = representations.get(0);
@ -354,6 +454,17 @@ public class DashChunkSourceTest extends InstrumentationTestCase {
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() {
List<Representation> representations = new ArrayList<>();
@ -363,103 +474,269 @@ public class DashChunkSourceTest extends InstrumentationTestCase {
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,
long periodStartMs, long durationMs) {
return generateMpd(true, Collections.singletonList(generateSegmentTimelineRepresentation(
segmentStartMs, periodStartMs, durationMs)), false);
}
private static MediaPresentationDescription generateLiveMpdWithTemplate(
boolean limitTimeshiftBuffer) {
List<Representation> representations = new ArrayList<>();
private static MediaPresentationDescription generateLiveMpdWithTemplate(long periodStartMs,
long periodDurationMs, boolean limitTimeshiftBuffer) {
return generateMpd(true, Collections.singletonList(generateSegmentTemplateRepresentation(
periodStartMs, periodDurationMs)), limitTimeshiftBuffer);
}
UrlTemplate initializationTemplate = null;
UrlTemplate mediaTemplate = UrlTemplate.compile("$RepresentationID$/$Number$");
MultiSegmentBase segmentBase = new SegmentTemplate(null, 1000, 0,
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);
private static MediaPresentationDescription generateMultiPeriodLiveMpdWithTimeline(
long startTimeMs) {
List<Period> periods = new ArrayList<Period>();
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,
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")
ManifestFetcher<MediaPresentationDescription> manifestFetcher = mock(ManifestFetcher.class);
when(manifestFetcher.getManifest()).thenReturn(mpd);
DashChunkSource chunkSource = new DashChunkSource(manifestFetcher, mpd,
AdaptationSet.TYPE_VIDEO, null, mockDataSource, EVALUATOR,
new FakeClock(AVAILABILITY_CURRENT_TIME_MS + periodStartMs), liveEdgeLatencyMs * 1000,
AVAILABILITY_REALTIME_OFFSET_MS * 1000, false, null, null);
new FakeClock(nowUs), liveEdgeLatencyMs * 1000, AVAILABILITY_REALTIME_OFFSET_MS * 1000,
false, null, null);
chunkSource.enable(0);
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);
assertEquals(startTimeUs, seekRangeValuesUs[0]);
assertEquals(endTimeUs, seekRangeValuesUs[1]);
}
private void checkLiveEdgeLatency(DashChunkSource chunkSource, List<MediaChunk> queue,
ChunkOperationHolder out, long seekPositionMs, long seekRangeStartMs, long seekRangeEndMs,
long chunkStartTimeMs, long chunkEndTimeMs) {
ChunkOperationHolder out, long seekPositionMs, long availableRangeStartMs,
long availableRangeEndMs, long chunkStartTimeMs, long chunkEndTimeMs) {
chunkSource.getChunkOperation(queue, seekPositionMs * 1000, 0, out);
TimeRange seekRange = chunkSource.getSeekRange();
TimeRange availableRange = chunkSource.getAvailableRange();
assertNotNull(out.chunk);
checkSeekRange(seekRange, seekRangeStartMs * 1000, seekRangeEndMs * 1000);
assertEquals(chunkStartTimeMs * 1000, ((MediaChunk) out.chunk).startTimeUs);
assertEquals(chunkEndTimeMs * 1000, ((MediaChunk) out.chunk).endTimeUs);
checkAvailableRange(availableRange, availableRangeStartMs * 1000, availableRangeEndMs * 1000);
if (chunkStartTimeMs < availableRangeEndMs) {
assertNotNull(out.chunk);
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,
long liveEdgeLatencyMs, long seekPositionMs, long seekRangeStartMs, long seekRangeEndMs,
long chunkStartTimeMs, long chunkEndTimeMs) {
long liveEdgeLatencyMs, long seekPositionMs, long availableRangeStartMs,
long availableRangeEndMs, long chunkStartTimeMs, long chunkEndTimeMs) {
DashChunkSource chunkSource = setupDashChunkSource(mpd, periodStartMs, liveEdgeLatencyMs);
List<MediaChunk> queue = new ArrayList<>();
ChunkOperationHolder out = new ChunkOperationHolder();
checkLiveEdgeLatency(chunkSource, queue, out, seekPositionMs, seekRangeStartMs, seekRangeEndMs,
chunkStartTimeMs, chunkEndTimeMs);
checkLiveEdgeLatency(chunkSource, queue, out, seekPositionMs, availableRangeStartMs,
availableRangeEndMs, chunkStartTimeMs, chunkEndTimeMs);
}
private void checkLiveEdgeLatencyWithTimeline(long segmentStartMs, long periodStartMs,
long liveEdgeLatencyMs, long seekPositionMs, long seekRangeStartMs, long seekRangeEndMs,
long chunkStartTimeMs, long chunkEndTimeMs) {
long liveEdgeLatencyMs, long seekPositionMs, long availableRangeStartMs,
long availableRangeEndMs, long chunkStartTimeMs, long chunkEndTimeMs) {
MediaPresentationDescription mpd = generateLiveMpdWithTimeline(segmentStartMs, periodStartMs,
LIVE_DURATION_MS);
checkLiveEdgeLatency(mpd, periodStartMs, liveEdgeLatencyMs, seekPositionMs, seekRangeStartMs,
seekRangeEndMs, chunkStartTimeMs, chunkEndTimeMs);
checkLiveEdgeLatency(mpd, periodStartMs, liveEdgeLatencyMs, seekPositionMs,
availableRangeStartMs, availableRangeEndMs, chunkStartTimeMs, chunkEndTimeMs);
}
private void checkLiveEdgeLatencyWithTemplateAndUnlimitedTimeshift(long startTimeMs,
long liveEdgeLatencyMs, long seekPositionMs, long seekRangeEndMs,
long liveEdgeLatencyMs, long availablePositionMs, long availableRangeEndMs,
long chunkStartTimeMs, long chunkEndTimeMs) {
MediaPresentationDescription mpd = generateLiveMpdWithTemplate(false);
checkLiveEdgeLatency(mpd, startTimeMs, liveEdgeLatencyMs, seekPositionMs, 0, seekRangeEndMs,
chunkStartTimeMs, chunkEndTimeMs);
MediaPresentationDescription mpd = generateLiveMpdWithTemplate(0,
TrackRenderer.UNKNOWN_TIME_US, false);
checkLiveEdgeLatency(mpd, startTimeMs, liveEdgeLatencyMs, availablePositionMs, 0,
availableRangeEndMs, chunkStartTimeMs, chunkEndTimeMs);
}
private void checkLiveEdgeLatencyWithTemplateAndLimitedTimeshift(long startTimeMs,
long liveEdgeLatencyMs, long seekPositionMs, long seekRangeStartMs, long seekRangeEndMs,
long chunkStartTimeMs, long chunkEndTimeMs) {
MediaPresentationDescription mpd = generateLiveMpdWithTemplate(true);
checkLiveEdgeLatency(mpd, startTimeMs, liveEdgeLatencyMs, seekPositionMs, seekRangeStartMs,
seekRangeEndMs, chunkStartTimeMs, chunkEndTimeMs);
long liveEdgeLatencyMs, long seekPositionMs, long availableRangeStartMs,
long availableRangeEndMs, long chunkStartTimeMs, long chunkEndTimeMs) {
MediaPresentationDescription mpd = generateLiveMpdWithTemplate(0,
TrackRenderer.UNKNOWN_TIME_US, true);
checkLiveEdgeLatency(mpd, startTimeMs, liveEdgeLatencyMs, seekPositionMs, availableRangeStartMs,
availableRangeEndMs, chunkStartTimeMs, chunkEndTimeMs);
}
private void checkLiveTimelineConsistency(long startTimeMs, long liveEdgeLatencyMs,
long seekPositionMs, long seekRangeStartMs, long seekRangeEndMs, long chunkStartTimeMs,
long chunkEndTimeMs) {
long seekPositionMs, long availableRangeStartMs, long availableRangeEndMs,
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,
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,
seekPositionMs, seekRangeEndMs, chunkStartTimeMs, chunkEndTimeMs);
seekPositionMs, availableRangeEndMs, chunkStartTimeMs, chunkEndTimeMs);
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);
}
}

View file

@ -38,6 +38,13 @@ public abstract class BaseMediaChunk extends MediaChunk {
private DefaultTrackOutput output;
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 dataSpec Defines the data to be loaded.
@ -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
* these methods are only guaranteed to return correct data after the first sample data has
* 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,
long startTimeUs, long endTimeUs, int chunkIndex, boolean isLastChunk,
boolean isMediaFormatFinal) {
super(dataSource, dataSpec, trigger, format, startTimeUs, endTimeUs, chunkIndex, isLastChunk);
boolean isMediaFormatFinal, int parentId) {
super(dataSource, dataSpec, trigger, format, startTimeUs, endTimeUs, chunkIndex, isLastChunk,
parentId);
this.isMediaFormatFinal = isMediaFormatFinal;
}

View file

@ -75,6 +75,10 @@ public abstract class Chunk implements Loadable {
* Implementations may define custom {@link #trigger} codes greater than or equal to this value.
*/
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.
@ -93,9 +97,17 @@ public abstract class Chunk implements Loadable {
* The {@link DataSpec} that defines the data to be loaded.
*/
public final DataSpec dataSpec;
/**
* Optional identifier for a parent from which this chunk originates.
*/
public final int parentId;
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 dataSpec Defines the data to be loaded. {@code dataSpec.length} must not exceed
@ -105,13 +117,16 @@ public abstract class Chunk implements Loadable {
* @param type See {@link #type}.
* @param trigger See {@link #trigger}.
* @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.dataSpec = Assertions.checkNotNull(dataSpec);
this.type = type;
this.trigger = trigger;
this.format = format;
this.parentId = parentId;
}
/**

View file

@ -43,6 +43,15 @@ public class ContainerMediaChunk extends BaseMediaChunk implements SingleTrackOu
private volatile int bytesLoaded;
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 dataSpec Defines the data to be loaded.
@ -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.
* @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.
* @param parentId Identifier for a parent from which this chunk originates.
*/
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) {
boolean isMediaFormatFinal, int parentId) {
super(dataSource, dataSpec, trigger, format, startTimeUs, endTimeUs, chunkIndex, isLastChunk,
isMediaFormatFinal);
isMediaFormatFinal, parentId);
this.extractorWrapper = extractorWrapper;
this.sampleOffsetUs = sampleOffsetUs;
this.mediaFormat = getAdjustedMediaFormat(mediaFormat, sampleOffsetUs);

View file

@ -46,6 +46,11 @@ public final class InitializationChunk extends Chunk implements SingleTrackOutpu
private volatile int bytesLoaded;
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.
*
@ -54,10 +59,11 @@ public final class InitializationChunk extends Chunk implements SingleTrackOutpu
* @param trigger The reason for this chunk being selected.
* @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 parentId Identifier for a parent from which this chunk originates.
*/
public InitializationChunk(DataSource dataSource, DataSpec dataSpec, int trigger, Format format,
ChunkExtractorWrapper extractorWrapper) {
super(dataSource, dataSpec, Chunk.TYPE_MEDIA_INITIALIZATION, trigger, format);
ChunkExtractorWrapper extractorWrapper, int parentId) {
super(dataSource, dataSpec, Chunk.TYPE_MEDIA_INITIALIZATION, trigger, format, parentId);
this.extractorWrapper = extractorWrapper;
}

View file

@ -41,6 +41,12 @@ public abstract class MediaChunk extends Chunk {
*/
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 dataSpec Defines the data to be loaded.
@ -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 chunkIndex The index of the chunk.
* @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,
long startTimeUs, long endTimeUs, int chunkIndex, boolean isLastChunk) {
super(dataSource, dataSpec, Chunk.TYPE_MEDIA, trigger, format);
long startTimeUs, long endTimeUs, int chunkIndex, boolean isLastChunk, int parentId) {
super(dataSource, dataSpec, Chunk.TYPE_MEDIA, trigger, format, parentId);
Assertions.checkNotNull(format);
this.startTimeUs = startTimeUs;
this.endTimeUs = endTimeUs;

View file

@ -35,6 +35,13 @@ public final class SingleSampleMediaChunk extends BaseMediaChunk {
private volatile int bytesLoaded;
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 dataSpec Defines the data to be loaded.
@ -47,12 +54,13 @@ public final class SingleSampleMediaChunk extends BaseMediaChunk {
* @param sampleFormat The format of the sample.
* @param sampleDrmInitData The {@link DrmInitData} for the sample. Null if the sample is not drm
* protected.
* @param parentId Identifier for a parent from which this chunk originates.
*/
public SingleSampleMediaChunk(DataSource dataSource, DataSpec dataSpec, int trigger,
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,
true);
true, parentId);
this.sampleFormat = sampleFormat;
this.sampleDrmInitData = sampleDrmInitData;
}

View file

@ -50,6 +50,7 @@ import com.google.android.exoplayer.util.MimeTypes;
import com.google.android.exoplayer.util.SystemClock;
import android.os.Handler;
import android.util.SparseArray;
import java.io.IOException;
import java.util.Arrays;
@ -61,7 +62,15 @@ import java.util.List;
* An {@link ChunkSource} for DASH streams.
* <p>
* 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 {
/**
@ -72,9 +81,9 @@ public class DashChunkSource implements ChunkSource {
/**
* 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 {
private final int maxWidth;
private final int maxHeight;
private final Format[] formats;
private final HashMap<String, RepresentationHolder> representationHolders;
private final SparseArray<PeriodHolder> periodHolders;
private final ManifestFetcher<MediaPresentationDescription> manifestFetcher;
private final int adaptationSetIndex;
private final int[] representationIndices;
private MediaPresentationDescription currentManifest;
private boolean finishedCurrentManifest;
private int periodHolderNextIndex;
private DrmInitData drmInitData;
private TimeRange seekRange;
private long[] seekRangeValues;
private int firstAvailableSegmentNum;
private int lastAvailableSegmentNum;
private TimeRange availableRange;
private long[] availableRangeValues;
private boolean startAtLiveEdge;
private boolean lastChunkWasInitialization;
@ -255,33 +262,36 @@ public class DashChunkSource implements ChunkSource {
this.eventHandler = eventHandler;
this.eventListener = eventListener;
this.evaluation = new Evaluation();
this.seekRangeValues = new long[2];
this.availableRangeValues = new long[2];
drmInitData = getDrmInitData(currentManifest, adaptationSetIndex);
Representation[] representations = getFilteredRepresentations(currentManifest,
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);
periodHolders = new SparseArray<>();
this.formats = new Format[representations.length];
this.representationHolders = new HashMap<>();
int maxWidth = 0;
processManifest(currentManifest);
String mimeType = "";
long totalDurationUs = 0;
int maxHeight = 0;
for (int i = 0; i < representations.length; i++) {
formats[i] = representations[i].format;
maxWidth = Math.max(formats[i].width, maxWidth);
maxHeight = Math.max(formats[i].height, maxHeight);
Extractor extractor = mimeTypeIsWebm(formats[i].mimeType) ? new WebmExtractor()
: new FragmentedMp4Extractor();
representationHolders.put(formats[i].id,
new RepresentationHolder(representations[i], new ChunkExtractorWrapper(extractor)));
int maxWidth = 0;
for (int i = 0; i < periodHolders.size(); i++) {
PeriodHolder periodHolder = periodHolders.valueAt(i);
if (totalDurationUs != TrackRenderer.UNKNOWN_TIME_US) {
if (periodHolder.durationUs == TrackRenderer.UNKNOWN_TIME_US) {
totalDurationUs = TrackRenderer.UNKNOWN_TIME_US;
} 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;
Arrays.sort(formats, new DecreasingBandwidthComparator());
this.maxWidth = maxWidth;
}
@Override
@ -306,8 +316,8 @@ public class DashChunkSource implements ChunkSource {
}
// VisibleForTesting
/* package */ TimeRange getSeekRange() {
return seekRange;
/* package */ TimeRange getAvailableRange() {
return availableRange;
}
@Override
@ -317,16 +327,7 @@ public class DashChunkSource implements ChunkSource {
if (manifestFetcher != null) {
manifestFetcher.enable();
}
DashSegmentIndex segmentIndex =
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);
}
updateAvailableBounds(getNowUs());
}
@Override
@ -335,7 +336,7 @@ public class DashChunkSource implements ChunkSource {
if (manifestFetcher != null) {
manifestFetcher.disable();
}
seekRange = null;
availableRange = null;
}
@Override
@ -346,41 +347,8 @@ public class DashChunkSource implements ChunkSource {
MediaPresentationDescription newManifest = manifestFetcher.getManifest();
if (currentManifest != newManifest && newManifest != null) {
Representation[] newRepresentations = DashChunkSource.getFilteredRepresentations(newManifest,
adaptationSetIndex, representationIndices);
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);
processManifest(newManifest);
updateAvailableBounds(getNowUs());
}
// TODO: This is a temporary hack to avoid constantly refreshing the MPD in cases where
@ -392,8 +360,8 @@ public class DashChunkSource implements ChunkSource {
minUpdatePeriod = 5000;
}
if (finishedCurrentManifest && (android.os.SystemClock.elapsedRealtime()
> manifestFetcher.getManifestLoadStartTimestamp() + minUpdatePeriod)) {
if (android.os.SystemClock.elapsedRealtime()
> manifestFetcher.getManifestLoadStartTimestamp() + minUpdatePeriod) {
manifestFetcher.requestRefresh();
}
}
@ -408,7 +376,14 @@ public class DashChunkSource implements ChunkSource {
evaluation.queueSize = queue.size();
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;
out.queueSize = evaluation.queueSize;
@ -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.
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;
DashSegmentIndex segmentIndex = representationHolder.segmentIndex;
ChunkExtractorWrapper extractorWrapper = representationHolder.extractorWrapper;
RangedUri pendingInitializationUri = null;
RangedUri pendingIndexUri = null;
if (representationHolder.format == null) {
MediaFormat mediaFormat = representationHolder.mediaFormat;
if (mediaFormat == null) {
pendingInitializationUri = selectedRepresentation.getInitializationUri();
}
if (segmentIndex == null) {
if (representationHolder.segmentIndex == null) {
pendingIndexUri = selectedRepresentation.getIndexUri();
}
if (pendingInitializationUri != null || pendingIndexUri != null) {
// We have initialization and/or index requests to make.
Chunk initializationChunk = newInitializationChunk(pendingInitializationUri, pendingIndexUri,
selectedRepresentation, extractorWrapper, dataSource, evaluation.trigger);
selectedRepresentation, extractorWrapper, dataSource, periodHolder.manifestIndex,
evaluation.trigger);
lastChunkWasInitialization = true;
out.chunk = initializationChunk;
return;
}
int segmentNum;
boolean indexUnbounded = segmentIndex.getLastSegmentNum() == DashSegmentIndex.INDEX_UNBOUNDED;
if (indexUnbounded) {
// 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; also if
// the bounds have changed, we should update the seek range
long nowUs = getNowUs();
int oldFirstAvailableSegmentNum = firstAvailableSegmentNum;
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 (startingNewPeriod) {
if (queue.isEmpty()) {
// when starting a new period (or beginning playback for the first time), the segment
// numbering might have been reset, so we'll need to determine the correct number from
// the representation holder itself
segmentNum = representationHolder.getSegmentNum(segmentStartTimeUs);
} else {
segmentNum = representationHolder.getFirstAvailableSegmentNum();
}
}
if (segmentNum == -1) {
// We've reached the end of the stream.
return;
}
Chunk nextMediaChunk = newMediaChunk(representationHolder, dataSource, segmentNum,
evaluation.trigger);
Chunk nextMediaChunk = newMediaChunk(periodHolder, representationHolder, dataSource,
mediaFormat, segmentNum, evaluation.trigger);
lastChunkWasInitialization = false;
out.chunk = nextMediaChunk;
}
@ -534,9 +535,16 @@ public class DashChunkSource implements ChunkSource {
if (chunk instanceof InitializationChunk) {
InitializationChunk initializationChunk = (InitializationChunk) chunk;
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()) {
representationHolder.format = initializationChunk.getFormat();
representationHolder.mediaFormat = initializationChunk.getFormat();
}
if (initializationChunk.hasSeekMap()) {
representationHolder.segmentIndex = new DashWrappingSegmentIndex(
@ -544,6 +552,7 @@ public class DashChunkSource implements ChunkSource {
initializationChunk.dataSpec.uri.toString(),
representationHolder.representation.periodStartMs * 1000);
}
// 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.
if (drmInitData == null && initializationChunk.hasDrmInitData()) {
@ -557,56 +566,42 @@ public class DashChunkSource implements ChunkSource {
// Do nothing.
}
private void updateAvailableSegmentBounds(DashSegmentIndex segmentIndex, long nowUs) {
int indexFirstAvailableSegmentNum = segmentIndex.getFirstSegmentNum();
int indexLastAvailableSegmentNum = segmentIndex.getLastSegmentNum();
if (indexLastAvailableSegmentNum == DashSegmentIndex.INDEX_UNBOUNDED) {
// The index is itself unbounded. We need to use the current time to calculate the range of
// available segments.
long liveEdgeTimestampUs = nowUs - currentManifest.availabilityStartTime * 1000;
private void updateAvailableBounds(long nowUs) {
PeriodHolder firstPeriod = periodHolders.valueAt(0);
long earliestAvailablePosition = firstPeriod.getAvailableStartTimeUs();
PeriodHolder lastPeriod = periodHolders.valueAt(periodHolders.size() - 1);
boolean isManifestUnbounded = lastPeriod.isIndexUnbounded();
long latestAvailablePosition;
if (!currentManifest.dynamic || !isManifestUnbounded) {
latestAvailablePosition = lastPeriod.getAvailableEndTimeUs();
} else {
latestAvailablePosition = TrackRenderer.UNKNOWN_TIME_US;
}
if (currentManifest.dynamic) {
if (isManifestUnbounded) {
latestAvailablePosition = nowUs - currentManifest.availabilityStartTime * 1000;
} else if (!lastPeriod.isIndexExplicit()) {
// Some segments defined by the index may not be available yet. Bound the calculated live
// edge based on the elapsed time since the manifest became available.
latestAvailablePosition = Math.min(latestAvailablePosition,
nowUs - currentManifest.availabilityStartTime * 1000);
}
// if we have a limited timeshift buffer, we need to adjust the earliest seek position so
// that it doesn't start before the buffer
if (currentManifest.timeShiftBufferDepth != -1) {
long bufferDepthUs = currentManifest.timeShiftBufferDepth * 1000;
indexFirstAvailableSegmentNum = Math.max(indexFirstAvailableSegmentNum,
segmentIndex.getSegmentNum(liveEdgeTimestampUs - bufferDepthUs));
earliestAvailablePosition = Math.max(earliestAvailablePosition,
latestAvailablePosition - 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) {
long liveEdgeTimestampUs;
if (segmentIndex.getLastSegmentNum() == DashSegmentIndex.INDEX_UNBOUNDED) {
liveEdgeTimestampUs = nowUs - currentManifest.availabilityStartTime * 1000;
} else {
liveEdgeTimestampUs = segmentIndex.getTimeUs(segmentIndex.getLastSegmentNum())
+ segmentIndex.getDurationUs(segmentIndex.getLastSegmentNum());
if (!segmentIndex.isExplicit()) {
// 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
// the earliest position in the case of a DVR-like stream that's just starting up, so
// in that case just return the earliest position instead
latestSeekPosition = Math.max(earliestSeekPosition, liveEdgeTimestampUs - liveEdgeLatencyUs);
}
TimeRange newSeekRange = new TimeRange(TimeRange.TYPE_SNAPSHOT, earliestSeekPosition,
latestSeekPosition);
if (seekRange == null || !seekRange.equals(newSeekRange)) {
seekRange = newSeekRange;
notifySeekRangeChanged(seekRange);
TimeRange newAvailableRange = new TimeRange(TimeRange.TYPE_SNAPSHOT, earliestAvailablePosition,
latestAvailablePosition);
if (availableRange == null || !availableRange.equals(newAvailableRange)) {
availableRange = newAvailableRange;
notifyAvailableRangeChanged(availableRange);
}
}
@ -616,7 +611,7 @@ public class DashChunkSource implements ChunkSource {
private Chunk newInitializationChunk(RangedUri initializationUri, RangedUri indexUri,
Representation representation, ChunkExtractorWrapper extractor, DataSource dataSource,
int trigger) {
int manifestIndex, int trigger) {
RangedUri requestUri;
if (initializationUri != null) {
// It's common for initialization and index data to be stored adjacently. Attempt to merge
@ -630,37 +625,36 @@ public class DashChunkSource implements ChunkSource {
}
DataSpec dataSpec = new DataSpec(requestUri.getUri(), requestUri.start, requestUri.length,
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,
int segmentNum, int trigger) {
private Chunk newMediaChunk(PeriodHolder periodHolder, RepresentationHolder representationHolder,
DataSource dataSource, MediaFormat mediaFormat, int segmentNum, int trigger) {
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
&& 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,
representation.getCacheKey());
long sampleOffsetUs = representation.periodStartMs * 1000
- representation.presentationTimeOffsetUs;
long sampleOffsetUs = periodHolder.startTimeUs - representation.presentationTimeOffsetUs;
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,
representation.format, startTimeUs, endTimeUs, absoluteSegmentNum, isLastSegment,
mediaFormat, null);
representation.format, startTimeUs, endTimeUs, segmentNum, isLastSegment,
MediaFormat.createTextFormat(MimeTypes.TEXT_VTT, MediaFormat.NO_VALUE,
representation.format.language), null, periodHolder.manifestIndex);
} else {
boolean isMediaFormatFinal = (mediaFormat != null);
return new ContainerMediaChunk(dataSource, dataSpec, trigger, representation.format,
startTimeUs, endTimeUs, absoluteSegmentNum, isLastSegment, sampleOffsetUs,
representationHolder.extractorWrapper, representationHolder.format, drmInitData, true);
startTimeUs, endTimeUs, segmentNum, isLastSegment, sampleOffsetUs,
representationHolder.extractorWrapper, mediaFormat, drmInitData, isMediaFormatFinal,
periodHolder.manifestIndex);
}
}
@ -682,23 +676,6 @@ public class DashChunkSource implements ChunkSource {
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,
int adaptationSetIndex) {
AdaptationSet adaptationSet = manifest.periods.get(0).adaptationSets.get(adaptationSetIndex);
@ -708,7 +685,8 @@ public class DashChunkSource implements ChunkSource {
return null;
} else {
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 (drmInitData == null) {
drmInitData = new DrmInitData.Mapped(drmInitMimeType);
@ -730,26 +708,96 @@ public class DashChunkSource implements ChunkSource {
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) {
eventHandler.post(new Runnable() {
@Override
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 Representation representation;
public DashSegmentIndex segmentIndex;
public MediaFormat format;
public MediaFormat mediaFormat;
public int segmentNumShift;
private int segmentNumShift;
public RepresentationHolder(Representation representation,
ChunkExtractorWrapper extractorWrapper) {
@ -758,6 +806,173 @@ public class DashChunkSource implements ChunkSource {
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;
}
}
}