mirror of
https://github.com/samsonjs/media.git
synced 2026-04-01 10:35:48 +00:00
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:
parent
f69f948991
commit
0efaec59b8
10 changed files with 912 additions and 365 deletions
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue