diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/EventLogger.java b/demo/src/main/java/com/google/android/exoplayer/demo/EventLogger.java index ff7ceecc98e..8d8ea7e55c3 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/EventLogger.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/EventLogger.java @@ -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 void onDecoderInitialized(String decoderName, long elapsedRealtimeMs, } @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) { diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/player/DemoPlayer.java b/demo/src/main/java/com/google/android/exoplayer/demo/player/DemoPlayer.java index f93b9bf5071..acefd9930a9 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/player/DemoPlayer.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/player/DemoPlayer.java @@ -124,7 +124,7 @@ void onLoadCompleted(int sourceId, long bytesLoaded, int type, int trigger, Form 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 void onMetadata(Map metadata) { } @Override - public void onSeekRangeChanged(TimeRange seekRange) { + public void onAvailableRangeChanged(TimeRange availableRange) { if (infoListener != null) { - infoListener.onSeekRangeChanged(seekRange); + infoListener.onAvailableRangeChanged(availableRange); } } diff --git a/library/src/androidTest/java/com/google/android/exoplayer/dash/DashChunkSourceTest.java b/library/src/androidTest/java/com/google/android/exoplayer/dash/DashChunkSourceTest.java index e9cab9afb58..4d1bd6376d7 100644 --- a/library/src/androidTest/java/com/google/android/exoplayer/dash/DashChunkSourceTest.java +++ b/library/src/androidTest/java/com/google/android/exoplayer/dash/DashChunkSourceTest.java @@ -25,6 +25,7 @@ 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 void testMaxVideoDimensions() { 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 queue = new ArrayList(); + 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 void testMaxVideoDimensionsLegacy() { 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 @@ private static Representation generateSegmentTimelineRepresentation(long segment 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 representations, boolean limitTimeshiftBuffer) { Representation firstRepresentation = representations.get(0); @@ -354,6 +454,17 @@ private static MediaPresentationDescription generateMpd(boolean live, Collections.singletonList(period)); } + private static MediaPresentationDescription generateMultiPeriodMpd(boolean live, + List 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 representations = new ArrayList<>(); @@ -363,103 +474,269 @@ private static MediaPresentationDescription generateVodMpd() { return generateMpd(false, representations, false); } + private MediaPresentationDescription generateMultiPeriodVodMpd() { + List 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 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 periods = new ArrayList(); - 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 periods = new ArrayList(); + + 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 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 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(); - - assertNotNull(out.chunk); - checkSeekRange(seekRange, seekRangeStartMs * 1000, seekRangeEndMs * 1000); - assertEquals(chunkStartTimeMs * 1000, ((MediaChunk) out.chunk).startTimeUs); - assertEquals(chunkEndTimeMs * 1000, ((MediaChunk) out.chunk).endTimeUs); + TimeRange availableRange = chunkSource.getAvailableRange(); + + 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 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 queue = new ArrayList(); + 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); } } diff --git a/library/src/main/java/com/google/android/exoplayer/chunk/BaseMediaChunk.java b/library/src/main/java/com/google/android/exoplayer/chunk/BaseMediaChunk.java index 04f8a084cd3..f8ffcf8c4a1 100644 --- a/library/src/main/java/com/google/android/exoplayer/chunk/BaseMediaChunk.java +++ b/library/src/main/java/com/google/android/exoplayer/chunk/BaseMediaChunk.java @@ -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; } diff --git a/library/src/main/java/com/google/android/exoplayer/chunk/Chunk.java b/library/src/main/java/com/google/android/exoplayer/chunk/Chunk.java index e42c2331fd3..548306cafe2 100644 --- a/library/src/main/java/com/google/android/exoplayer/chunk/Chunk.java +++ b/library/src/main/java/com/google/android/exoplayer/chunk/Chunk.java @@ -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; } /** diff --git a/library/src/main/java/com/google/android/exoplayer/chunk/ContainerMediaChunk.java b/library/src/main/java/com/google/android/exoplayer/chunk/ContainerMediaChunk.java index dd542c8616a..7a9199bce94 100644 --- a/library/src/main/java/com/google/android/exoplayer/chunk/ContainerMediaChunk.java +++ b/library/src/main/java/com/google/android/exoplayer/chunk/ContainerMediaChunk.java @@ -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); diff --git a/library/src/main/java/com/google/android/exoplayer/chunk/InitializationChunk.java b/library/src/main/java/com/google/android/exoplayer/chunk/InitializationChunk.java index dd6c68c7491..2a953a185f1 100644 --- a/library/src/main/java/com/google/android/exoplayer/chunk/InitializationChunk.java +++ b/library/src/main/java/com/google/android/exoplayer/chunk/InitializationChunk.java @@ -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; } diff --git a/library/src/main/java/com/google/android/exoplayer/chunk/MediaChunk.java b/library/src/main/java/com/google/android/exoplayer/chunk/MediaChunk.java index f7d3812a7a6..865dbef85d1 100644 --- a/library/src/main/java/com/google/android/exoplayer/chunk/MediaChunk.java +++ b/library/src/main/java/com/google/android/exoplayer/chunk/MediaChunk.java @@ -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; diff --git a/library/src/main/java/com/google/android/exoplayer/chunk/SingleSampleMediaChunk.java b/library/src/main/java/com/google/android/exoplayer/chunk/SingleSampleMediaChunk.java index 064638580b4..e7dab3c3ad5 100644 --- a/library/src/main/java/com/google/android/exoplayer/chunk/SingleSampleMediaChunk.java +++ b/library/src/main/java/com/google/android/exoplayer/chunk/SingleSampleMediaChunk.java @@ -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; } diff --git a/library/src/main/java/com/google/android/exoplayer/dash/DashChunkSource.java b/library/src/main/java/com/google/android/exoplayer/dash/DashChunkSource.java index 15f3b160e97..604be78b599 100644 --- a/library/src/main/java/com/google/android/exoplayer/dash/DashChunkSource.java +++ b/library/src/main/java/com/google/android/exoplayer/dash/DashChunkSource.java @@ -50,6 +50,7 @@ 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 @@ * An {@link ChunkSource} for DASH streams. *

* This implementation currently supports fMP4, webm, and webvtt. + *

+ * This implementation makes the following assumptions about multi-period manifests: + *

    + *
  1. that new periods will contain the same representations as previous periods (i.e. no new or + * missing representations) and
  2. + *
  3. that representations are contiguous across multiple periods
  4. + *
*/ +// TODO: handle cases where the above assumption are false public class DashChunkSource implements ChunkSource { /** @@ -72,9 +81,9 @@ public interface EventListener { /** * 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 NoAdaptationSetException(String message) { private final int maxWidth; private final int maxHeight; - private final Format[] formats; - private final HashMap representationHolders; + private final SparseArray periodHolders; private final ManifestFetcher 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 @@ dataSource, formatEvaluator, new SystemClock(), liveEdgeLatencyMs * 1000, 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 final MediaFormat getFormat(int track) { } // VisibleForTesting - /* package */ TimeRange getSeekRange() { - return seekRange; + /* package */ TimeRange getAvailableRange() { + return availableRange; } @Override @@ -317,16 +327,7 @@ public void enable(int track) { 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 void disable(List queue) { if (manifestFetcher != null) { manifestFetcher.disable(); } - seekRange = null; + availableRange = null; } @Override @@ -346,41 +347,8 @@ public void continueBuffering(long playbackPositionUs) { 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 void continueBuffering(long playbackPositionUs) { 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 final void getChunkOperation(List queue, long seekP 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 final void getChunkOperation(List queue, long seekP // 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 void onChunkLoadCompleted(Chunk chunk) { 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 void onChunkLoadCompleted(Chunk chunk) { 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 void onChunkLoadError(Chunk chunk, Exception e) { // 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; - if (currentManifest.timeShiftBufferDepth != -1) { - long bufferDepthUs = currentManifest.timeShiftBufferDepth * 1000; - indexFirstAvailableSegmentNum = Math.max(indexFirstAvailableSegmentNum, - segmentIndex.getSegmentNum(liveEdgeTimestampUs - bufferDepthUs)); - } - // getSegmentNum(liveEdgeTimestampUs) will not be completed yet, so subtract one to get the - // index of the last completed segment. - indexLastAvailableSegmentNum = segmentIndex.getSegmentNum(liveEdgeTimestampUs) - 1; + 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; } - 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); - } + 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); } - // 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); + // if we have a limited timeshift buffer, we need to adjust the earliest seek position so + // that it doesn't start before the buffer + if (currentManifest.timeShiftBufferDepth != -1) { + long bufferDepthUs = currentManifest.timeShiftBufferDepth * 1000; + earliestAvailablePosition = Math.max(earliestAvailablePosition, + latestAvailablePosition - bufferDepthUs); + } } - TimeRange 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 @@ private static boolean mimeTypeIsWebm(String mimeType) { 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 @@ private Chunk newInitializationChunk(RangedUri initializationUri, RangedUri inde } 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 @@ private static String getMediaMimeType(Representation representation) { return mimeType; } - private static Representation[] getFilteredRepresentations(MediaPresentationDescription manifest, - int adaptationSetIndex, int[] representationIndices) { - AdaptationSet adaptationSet = manifest.periods.get(0).adaptationSets.get(adaptationSetIndex); - List 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 @@ private static DrmInitData getDrmInitData(MediaPresentationDescription manifest, 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 @@ private static MediaPresentationDescription buildManifest(List r 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 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 RepresentationHolder(Representation representation, 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 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; + } + } }