diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/player/DashRendererBuilder.java b/demo/src/main/java/com/google/android/exoplayer/demo/player/DashRendererBuilder.java index b6ceb30be3..ef9433b3f0 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/player/DashRendererBuilder.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/player/DashRendererBuilder.java @@ -188,7 +188,7 @@ public class DashRendererBuilder implements RendererBuilder { } private void buildRenderers() { - Period period = manifest.periods.get(0); + Period period = manifest.getPeriod(0); Handler mainHandler = player.getMainHandler(); LoadControl loadControl = new DefaultLoadControl(new DefaultAllocator(BUFFER_SEGMENT_SIZE)); DefaultBandwidthMeter bandwidthMeter = new DefaultBandwidthMeter(mainHandler, player); diff --git a/demo_misc/webm_sw_decoder/src/main/java/com/google/android/exoplayer/demo/webm/DashRendererBuilder.java b/demo_misc/webm_sw_decoder/src/main/java/com/google/android/exoplayer/demo/webm/DashRendererBuilder.java index 377d7611ae..7c35fcfa63 100644 --- a/demo_misc/webm_sw_decoder/src/main/java/com/google/android/exoplayer/demo/webm/DashRendererBuilder.java +++ b/demo_misc/webm_sw_decoder/src/main/java/com/google/android/exoplayer/demo/webm/DashRendererBuilder.java @@ -24,7 +24,6 @@ import com.google.android.exoplayer.chunk.ChunkSampleSource; import com.google.android.exoplayer.chunk.ChunkSource; import com.google.android.exoplayer.chunk.FormatEvaluator; import com.google.android.exoplayer.chunk.FormatEvaluator.AdaptiveEvaluator; -import com.google.android.exoplayer.chunk.MultiTrackChunkSource; import com.google.android.exoplayer.dash.DashChunkSource; import com.google.android.exoplayer.dash.mpd.AdaptationSet; import com.google.android.exoplayer.dash.mpd.MediaPresentationDescription; @@ -83,16 +82,16 @@ public class DashRendererBuilder implements ManifestCallback audioRepresentationsList = new ArrayList<>(); + Representation audioRepresentation = null; ArrayList videoRepresentationsList = new ArrayList<>(); - Period period = manifest.periods.get(0); + Period period = manifest.getPeriod(0); for (int i = 0; i < period.adaptationSets.size(); i++) { AdaptationSet adaptationSet = period.adaptationSets.get(i); int adaptationSetType = adaptationSet.type; for (int j = 0; j < adaptationSet.representations.size(); j++) { Representation representation = adaptationSet.representations.get(j); - if (adaptationSetType == AdaptationSet.TYPE_AUDIO) { - audioRepresentationsList.add(representation); + if (adaptationSetType == AdaptationSet.TYPE_AUDIO && audioRepresentation == null) { + audioRepresentation = representation; } else if (adaptationSetType == AdaptationSet.TYPE_VIDEO) { videoRepresentationsList.add(representation); } @@ -109,7 +108,8 @@ public class DashRendererBuilder implements ManifestCallback queue = new ArrayList<>(); @@ -166,394 +135,258 @@ public class DashChunkSourceTest extends InstrumentationTestCase { } public void testSegmentRequestSequenceOnMultiPeriodLiveWithTimeline() { - long liveEdgeLatency = 0; - MediaPresentationDescription mpd = generateMultiPeriodLiveMpdWithTimeline(0); - DashChunkSource chunkSource = setupDashChunkSource(mpd, 0, liveEdgeLatency); - + MediaPresentationDescription mpd = buildMultiPeriodLiveMpdWithTimeline(); + DashChunkSource chunkSource = buildDashChunkSource(mpd); 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); - + MediaPresentationDescription mpd = buildMultiPeriodLiveMpdWithTemplate(); + DashChunkSource chunkSource = buildDashChunkSource(mpd); checkSegmentRequestSequenceOnMultiPeriodLive(chunkSource); } - - public void testLiveEdgeNoLatency() { - long startTimeMs = 0; - long liveEdgeLatencyMs = 0; - long seekPositionMs = startTimeMs + LIVE_DURATION_MS - liveEdgeLatencyMs; + public void testLiveEdgeLatency() { long availableRangeStartMs = 0; long availableRangeEndMs = LIVE_DURATION_MS; + long seekPositionMs = LIVE_DURATION_MS; + long chunkStartTimeMs = 4000; long chunkEndTimeMs = 5000; + // Test with 1-1000ms latency. + long liveEdgeLatency = 1; + checkLiveEdgeConsistency(LIVE_DURATION_MS, 0, liveEdgeLatency, seekPositionMs, + availableRangeStartMs, availableRangeEndMs, chunkStartTimeMs, chunkEndTimeMs); + liveEdgeLatency = 1000; + checkLiveEdgeConsistency(LIVE_DURATION_MS, 0, liveEdgeLatency, seekPositionMs, + availableRangeStartMs, availableRangeEndMs, chunkStartTimeMs, chunkEndTimeMs); - checkLiveTimelineConsistency(startTimeMs, liveEdgeLatencyMs, seekPositionMs, + chunkStartTimeMs = 3000; + chunkEndTimeMs = 4000; + // Test with 1001-2000ms latency. + liveEdgeLatency = 1001; + checkLiveEdgeConsistency(LIVE_DURATION_MS, 0, liveEdgeLatency, seekPositionMs, + availableRangeStartMs, availableRangeEndMs, chunkStartTimeMs, chunkEndTimeMs); + liveEdgeLatency = 2000; + checkLiveEdgeConsistency(LIVE_DURATION_MS, 0, liveEdgeLatency, seekPositionMs, + availableRangeStartMs, availableRangeEndMs, chunkStartTimeMs, chunkEndTimeMs); + + chunkStartTimeMs = 0; + chunkEndTimeMs = 1000; + // Test with 9001-10000 latency. + liveEdgeLatency = 9001; + checkLiveEdgeConsistency(LIVE_DURATION_MS, 0, liveEdgeLatency, seekPositionMs, + availableRangeStartMs, availableRangeEndMs, chunkStartTimeMs, chunkEndTimeMs); + liveEdgeLatency = 10000; + checkLiveEdgeConsistency(LIVE_DURATION_MS, 0, liveEdgeLatency, seekPositionMs, + availableRangeStartMs, availableRangeEndMs, chunkStartTimeMs, chunkEndTimeMs); + + // Test with 10001 latency. Seek position will be bounded to the first chunk. + liveEdgeLatency = 10001; + checkLiveEdgeConsistency(LIVE_DURATION_MS, 0, liveEdgeLatency, seekPositionMs, availableRangeStartMs, availableRangeEndMs, chunkStartTimeMs, chunkEndTimeMs); } - public void testLiveEdgeAlmostNoLatency() { - long startTimeMs = 0; - long liveEdgeLatencyMs = 1; - long seekPositionMs = startTimeMs + LIVE_DURATION_MS - liveEdgeLatencyMs; - long availableRangeStartMs = 0; - long availableRangeEndMs = LIVE_DURATION_MS; - long chunkStartTimeMs = 4000; - long chunkEndTimeMs = 5000; + // Private methods. - checkLiveTimelineConsistency(startTimeMs, liveEdgeLatencyMs, seekPositionMs, - availableRangeStartMs, availableRangeEndMs, chunkStartTimeMs, chunkEndTimeMs); - } - - public void testLiveEdge500msLatency() { - long startTimeMs = 0; - long liveEdgeLatencyMs = 500; - 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, - availableRangeStartMs, availableRangeEndMs, chunkStartTimeMs, chunkEndTimeMs); - } - - public void testLiveEdge1000msLatency() { - long startTimeMs = 0; - long liveEdgeLatencyMs = 1000; - 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, - availableRangeStartMs, availableRangeEndMs, chunkStartTimeMs, chunkEndTimeMs); - } - - public void testLiveEdge1001msLatency() { - long startTimeMs = 0; - long liveEdgeLatencyMs = 1001; - 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, - availableRangeStartMs, availableRangeEndMs, chunkStartTimeMs, chunkEndTimeMs); - } - - public void testLiveEdge2500msLatency() { - long startTimeMs = 0; - long liveEdgeLatencyMs = 2500; - 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, - availableRangeStartMs, availableRangeEndMs, chunkStartTimeMs, chunkEndTimeMs); - } - - public void testLiveEdgeVeryHighLatency() { - long startTimeMs = 0; - long liveEdgeLatencyMs = 10000; - 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, - availableRangeStartMs, availableRangeEndMs, chunkStartTimeMs, chunkEndTimeMs); - } - - public void testLiveEdgeNoLatencyInProgress() { - long startTimeMs = 3000; - long liveEdgeLatencyMs = 0; - 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, - availableRangeStartMs, availableRangeEndMs, chunkStartTimeMs, chunkEndTimeMs); - } - - public void testLiveEdgeAlmostNoLatencyInProgress() { - long startTimeMs = 3000; - long liveEdgeLatencyMs = 1; - 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, - availableRangeStartMs, availableRangeEndMs, chunkStartTimeMs, chunkEndTimeMs); - } - - public void testLiveEdge500msLatencyInProgress() { - long startTimeMs = 3000; - long liveEdgeLatencyMs = 500; - 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, - availableRangeStartMs, availableRangeEndMs, chunkStartTimeMs, chunkEndTimeMs); - } - - public void testLiveEdge1000msLatencyInProgress() { - long startTimeMs = 3000; - long liveEdgeLatencyMs = 1000; - 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, - availableRangeStartMs, availableRangeEndMs, chunkStartTimeMs, chunkEndTimeMs); - } - - public void testLiveEdge1001msLatencyInProgress() { - long startTimeMs = 3000; - long liveEdgeLatencyMs = 1001; - 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, - availableRangeStartMs, availableRangeEndMs, chunkStartTimeMs, chunkEndTimeMs); - } - - public void testLiveEdge2500msLatencyInProgress() { - long startTimeMs = 3000; - long liveEdgeLatencyMs = 2500; - 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, - availableRangeStartMs, availableRangeEndMs, chunkStartTimeMs, chunkEndTimeMs); - } - - public void testLiveEdgeVeryHighLatencyInProgress() { - long startTimeMs = 3000; - long liveEdgeLatencyMs = 10000; - 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, - availableRangeStartMs, availableRangeEndMs, chunkStartTimeMs, chunkEndTimeMs); - checkLiveEdgeLatencyWithTimeline(0, startTimeMs, liveEdgeLatencyMs, seekPositionMs, - availableRangeStartMs, availableRangeEndMs, chunkStartTimeMs, chunkEndTimeMs); - checkLiveEdgeLatencyWithTemplateAndUnlimitedTimeshift(startTimeMs, liveEdgeLatencyMs, - 0, availableRangeEndMs, 0, 1000); - checkLiveEdgeLatencyWithTemplateAndLimitedTimeshift(startTimeMs, liveEdgeLatencyMs, - seekPositionMs, availableRangeStartMs, availableRangeEndMs, chunkStartTimeMs, - chunkEndTimeMs); - } - - private static Representation generateVodRepresentation(long startTimeMs, long duration, - Format format) { + private static Representation buildVodRepresentation(Format format) { RangedUri rangedUri = new RangedUri("https://example.com/1.mp4", null, 0, 100); SingleSegmentBase segmentBase = new SingleSegmentBase(rangedUri, 1, 0, "https://example.com/1.mp4", 0, -1); - return Representation.newInstance(startTimeMs, duration, null, 0, format, segmentBase); + return Representation.newInstance(null, 0, format, segmentBase); } - private static Representation generateSegmentTimelineRepresentation(long segmentStartMs, - long periodStartMs, long duration) { + private static Representation buildSegmentTimelineRepresentation(long timelineDurationMs, + long timelineStartTimeMs) { List segmentTimeline = new ArrayList<>(); List mediaSegments = new ArrayList<>(); - long segmentStartTimeMs = segmentStartMs; + long segmentStartTimeMs = timelineStartTimeMs; long byteStart = 0; - for (int i = 0; i < (duration / LIVE_SEGMENT_DURATION_MS); i++) { + // Create all but the last segment with LIVE_SEGMENT_DURATION_MS. + int segmentCount = (int) Util.ceilDivide(timelineDurationMs, LIVE_SEGMENT_DURATION_MS); + for (int i = 0; i < segmentCount - 1; i++) { segmentTimeline.add(new SegmentTimelineElement(segmentStartTimeMs, LIVE_SEGMENT_DURATION_MS)); mediaSegments.add(new RangedUri("", "", byteStart, 500L)); segmentStartTimeMs += LIVE_SEGMENT_DURATION_MS; byteStart += 500; } - - int startNumber = (int) ((periodStartMs + segmentStartMs) / LIVE_SEGMENT_DURATION_MS); - MultiSegmentBase segmentBase = new SegmentList(null, 1000, 0, - TrackRenderer.UNKNOWN_TIME_US, startNumber, TrackRenderer.UNKNOWN_TIME_US, segmentTimeline, + // The final segment duration is calculated so that the total duration is timelineDurationMs. + long finalSegmentDurationMs = (timelineStartTimeMs + timelineDurationMs) - segmentStartTimeMs; + segmentTimeline.add(new SegmentTimelineElement(segmentStartTimeMs, finalSegmentDurationMs)); + mediaSegments.add(new RangedUri("", "", byteStart, 500L)); + segmentStartTimeMs += finalSegmentDurationMs; + byteStart += 500; + // Construct the list. + MultiSegmentBase segmentBase = new SegmentList(null, 1000, 0, 0, 0, segmentTimeline, mediaSegments); - return Representation.newInstance(periodStartMs, TrackRenderer.UNKNOWN_TIME_US, null, 0, - REGULAR_VIDEO, segmentBase); + return Representation.newInstance(null, 0, REGULAR_VIDEO, segmentBase); } - private static Representation generateSegmentTemplateRepresentation(long periodStartMs, - long periodDurationMs) { + private static Representation buildSegmentTemplateRepresentation() { 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); + MultiSegmentBase segmentBase = new SegmentTemplate(null, 1000, 0, 0, LIVE_SEGMENT_DURATION_MS, + null, initializationTemplate, mediaTemplate, "http://www.youtube.com"); + return Representation.newInstance(null, 0, REGULAR_VIDEO, segmentBase); } - private static MediaPresentationDescription generateMpd(boolean live, - List representations, boolean limitTimeshiftBuffer) { - Representation firstRepresentation = representations.get(0); + private static MediaPresentationDescription buildMpd(long durationMs, + List representations, boolean live, boolean limitTimeshiftBuffer) { AdaptationSet adaptationSet = new AdaptationSet(0, AdaptationSet.TYPE_UNKNOWN, representations); - Period period = new Period(null, firstRepresentation.periodStartMs, - firstRepresentation.periodDurationMs, Collections.singletonList(adaptationSet)); - long duration = (live) ? TrackRenderer.UNKNOWN_TIME_US - : firstRepresentation.periodDurationMs - firstRepresentation.periodStartMs; - return new MediaPresentationDescription(AVAILABILITY_START_TIME_MS, duration, -1, live, -1, + Period period = new Period(null, 0, Collections.singletonList(adaptationSet)); + return new MediaPresentationDescription(AVAILABILITY_START_TIME_MS, durationMs, -1, live, -1, (limitTimeshiftBuffer) ? LIVE_TIMESHIFT_BUFFER_DEPTH_MS : -1, null, null, 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, + private static MediaPresentationDescription buildMultiPeriodMpd(long durationMs, + List periods, boolean live, boolean limitTimeshiftBuffer) { + return new MediaPresentationDescription(AVAILABILITY_START_TIME_MS, durationMs, -1, live, -1, (limitTimeshiftBuffer) ? LIVE_TIMESHIFT_BUFFER_DEPTH_MS : -1, null, null, periods); } - private static MediaPresentationDescription generateVodMpd() { + private static MediaPresentationDescription buildVodMpd() { List representations = new ArrayList<>(); - - representations.add(generateVodRepresentation(0, VOD_DURATION_MS, TALL_VIDEO)); - representations.add(generateVodRepresentation(0, VOD_DURATION_MS, WIDE_VIDEO)); - - return generateMpd(false, representations, false); + representations.add(buildVodRepresentation(TALL_VIDEO)); + representations.add(buildVodRepresentation(WIDE_VIDEO)); + return buildMpd(VOD_DURATION_MS, representations, false, false); } - private MediaPresentationDescription generateMultiPeriodVodMpd() { + private static MediaPresentationDescription buildMultiPeriodVodMpd() { List periods = new ArrayList<>(); - long startTimeMs = 0; - - long duration = VOD_DURATION_MS; + long timeMs = 0; + long periodDurationMs = VOD_DURATION_MS; for (int i = 0; i < 2; i++) { - Representation representation = generateVodRepresentation(startTimeMs, duration, - REGULAR_VIDEO); + Representation representation = buildVodRepresentation(REGULAR_VIDEO); AdaptationSet adaptationSet = new AdaptationSet(0, AdaptationSet.TYPE_UNKNOWN, Collections.singletonList(representation)); - Period period = new Period(null, startTimeMs, duration, - Collections.singletonList(adaptationSet)); + Period period = new Period(null, timeMs, Collections.singletonList(adaptationSet)); periods.add(period); - startTimeMs += duration; + timeMs += periodDurationMs; } - - return generateMultiPeriodMpd(false, periods, false); + return buildMultiPeriodMpd(timeMs, periods, false, false); } - private static MediaPresentationDescription generateLiveMpdWithTimeline(long segmentStartMs, - long periodStartMs, long durationMs) { - return generateMpd(true, Collections.singletonList(generateSegmentTimelineRepresentation( - segmentStartMs, periodStartMs, durationMs)), false); + private static MediaPresentationDescription buildLiveMpdWithTimeline(long durationMs, + long timelineStartTimeMs) { + Representation representation = buildSegmentTimelineRepresentation( + durationMs - timelineStartTimeMs, timelineStartTimeMs); + return buildMpd(durationMs, Collections.singletonList(representation), true, false); } - private static MediaPresentationDescription generateLiveMpdWithTemplate(long periodStartMs, - long periodDurationMs, boolean limitTimeshiftBuffer) { - return generateMpd(true, Collections.singletonList(generateSegmentTemplateRepresentation( - periodStartMs, periodDurationMs)), limitTimeshiftBuffer); + private static MediaPresentationDescription buildLiveMpdWithTemplate(long durationMs, + boolean limitTimeshiftBuffer) { + Representation representation = buildSegmentTemplateRepresentation(); + return buildMpd(durationMs, Collections.singletonList(representation), true, + limitTimeshiftBuffer); } - private static MediaPresentationDescription generateMultiPeriodLiveMpdWithTimeline( - long startTimeMs) { + private static MediaPresentationDescription buildMultiPeriodLiveMpdWithTimeline() { List periods = new ArrayList<>(); - + long periodStartTimeMs = 0; + long periodDurationMs = LIVE_DURATION_MS; for (int i = 0; i < MULTI_PERIOD_COUNT; i++) { - Representation representation = generateSegmentTimelineRepresentation(0, startTimeMs, - LIVE_DURATION_MS); + Representation representation = buildSegmentTimelineRepresentation(LIVE_DURATION_MS, 0); 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)); + Period period = new Period(null, periodStartTimeMs, Collections.singletonList(adaptationSet)); periods.add(period); - startTimeMs += LIVE_DURATION_MS; + periodStartTimeMs += periodDurationMs; } - - return generateMultiPeriodMpd(true, periods, false); + return buildMultiPeriodMpd(periodDurationMs, periods, true, false); } - private static MediaPresentationDescription generateMultiPeriodLiveMpdWithTemplate( - long periodStartTimeMs) { + private static MediaPresentationDescription buildMultiPeriodLiveMpdWithTemplate() { 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); + long periodStartTimeMs = 0; + long periodDurationMs = LIVE_DURATION_MS; + for (int i = 0; i < MULTI_PERIOD_COUNT; i++) { + Representation representation = buildSegmentTemplateRepresentation(); + AdaptationSet adaptationSet = new AdaptationSet(0, AdaptationSet.TYPE_UNKNOWN, + Collections.singletonList(representation)); + Period period = new Period(null, periodStartTimeMs, Collections.singletonList(adaptationSet)); + periods.add(period); + periodStartTimeMs += periodDurationMs; + } + return buildMultiPeriodMpd(MULTI_PERIOD_LIVE_DURATION_MS, periods, true, false); } - private DashChunkSource setupDashChunkSource(MediaPresentationDescription mpd, long periodStartMs, - long liveEdgeLatencyMs) { - return setupDashChunkSource(mpd, periodStartMs, liveEdgeLatencyMs, - AVAILABILITY_CURRENT_TIME_MS + periodStartMs); + private static DashChunkSource buildDashChunkSource(MediaPresentationDescription mpd) { + return buildDashChunkSource(mpd, false, 0); } - @SuppressWarnings("unused") - private DashChunkSource setupDashChunkSource(MediaPresentationDescription mpd, long periodStartMs, - long liveEdgeLatencyMs, long nowUs) { + private static DashChunkSource buildDashChunkSource(MediaPresentationDescription mpd, + boolean startAtLiveEdge, long liveEdgeLatencyMs) { @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(nowUs), liveEdgeLatencyMs * 1000, AVAILABILITY_REALTIME_OFFSET_MS * 1000, - false, null, null); + AdaptationSet.TYPE_VIDEO, null, mock(DataSource.class), EVALUATOR, + new FakeClock(mpd.availabilityStartTime + mpd.duration - ELAPSED_REALTIME_OFFSET_MS), + liveEdgeLatencyMs * 1000, ELAPSED_REALTIME_OFFSET_MS * 1000, startAtLiveEdge, null, null); chunkSource.enable(0); return chunkSource; } - private void checkAvailableRange(TimeRange seekRange, long startTimeUs, long endTimeUs) { + private static 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, + private static void checkLiveEdgeConsistency(long durationMs, long timelineStartMs, + long liveEdgeLatencyMs, long seekPositionMs, long availableRangeStartMs, + long availableRangeEndMs, long chunkStartTimeMs, long chunkEndTimeMs) { + checkLiveEdgeConsistencyWithTimeline(durationMs, timelineStartMs, liveEdgeLatencyMs, + seekPositionMs, availableRangeStartMs, availableRangeEndMs, chunkStartTimeMs, + chunkEndTimeMs); + checkLiveEdgeConsistencyWithTemplateAndUnlimitedTimeshift(durationMs, liveEdgeLatencyMs, + seekPositionMs, availableRangeEndMs, chunkStartTimeMs, chunkEndTimeMs); + checkLiveEdgeConsistencyWithTemplateAndLimitedTimeshift(durationMs, liveEdgeLatencyMs, + seekPositionMs, availableRangeStartMs, availableRangeEndMs, chunkStartTimeMs, + chunkEndTimeMs); + } + + private static void checkLiveEdgeConsistencyWithTimeline(long durationMs, long timelineStartMs, + long liveEdgeLatencyMs, long seekPositionMs, long availableRangeStartMs, + long availableRangeEndMs, long chunkStartTimeMs, long chunkEndTimeMs) { + MediaPresentationDescription mpd = buildLiveMpdWithTimeline(durationMs, timelineStartMs); + checkLiveEdgeConsistency(mpd, liveEdgeLatencyMs, seekPositionMs, + availableRangeStartMs, availableRangeEndMs, chunkStartTimeMs, chunkEndTimeMs); + } + + private static void checkLiveEdgeConsistencyWithTemplateAndUnlimitedTimeshift(long durationMs, + long liveEdgeLatencyMs, long availablePositionMs, long availableRangeEndMs, + long chunkStartTimeMs, long chunkEndTimeMs) { + MediaPresentationDescription mpd = buildLiveMpdWithTemplate(durationMs, false); + checkLiveEdgeConsistency(mpd, liveEdgeLatencyMs, availablePositionMs, 0, + availableRangeEndMs, chunkStartTimeMs, chunkEndTimeMs); + } + + private static void checkLiveEdgeConsistencyWithTemplateAndLimitedTimeshift(long durationMs, + long liveEdgeLatencyMs, long seekPositionMs, long availableRangeStartMs, + long availableRangeEndMs, long chunkStartTimeMs, long chunkEndTimeMs) { + MediaPresentationDescription mpd = buildLiveMpdWithTemplate(durationMs, true); + checkLiveEdgeConsistency(mpd, liveEdgeLatencyMs, seekPositionMs, availableRangeStartMs, + availableRangeEndMs, chunkStartTimeMs, chunkEndTimeMs); + } + + private static void checkLiveEdgeConsistency(MediaPresentationDescription mpd, + long liveEdgeLatencyMs, long seekPositionMs, long availableRangeStartMs, + long availableRangeEndMs, long chunkStartTimeMs, long chunkEndTimeMs) { + DashChunkSource chunkSource = buildDashChunkSource(mpd, true, liveEdgeLatencyMs); + List queue = new ArrayList<>(); + ChunkOperationHolder out = new ChunkOperationHolder(); + checkLiveEdgeConsistency(chunkSource, queue, out, seekPositionMs, availableRangeStartMs, + availableRangeEndMs, chunkStartTimeMs, chunkEndTimeMs); + } + + private static void checkLiveEdgeConsistency(DashChunkSource chunkSource, List queue, ChunkOperationHolder out, long seekPositionMs, long availableRangeStartMs, long availableRangeEndMs, long chunkStartTimeMs, long chunkEndTimeMs) { chunkSource.getChunkOperation(queue, seekPositionMs * 1000, 0, out); TimeRange availableRange = chunkSource.getAvailableRange(); - checkAvailableRange(availableRange, availableRangeStartMs * 1000, availableRangeEndMs * 1000); if (chunkStartTimeMs < availableRangeEndMs) { assertNotNull(out.chunk); @@ -564,62 +397,7 @@ public class DashChunkSourceTest extends InstrumentationTestCase { } } - private void checkLiveEdgeLatency(MediaPresentationDescription mpd, long periodStartMs, - 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, availableRangeStartMs, - availableRangeEndMs, chunkStartTimeMs, chunkEndTimeMs); - } - - private void checkLiveEdgeLatencyWithTimeline(long segmentStartMs, long periodStartMs, - 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, - availableRangeStartMs, availableRangeEndMs, chunkStartTimeMs, chunkEndTimeMs); - } - - private void checkLiveEdgeLatencyWithTemplateAndUnlimitedTimeshift(long startTimeMs, - long liveEdgeLatencyMs, long availablePositionMs, long availableRangeEndMs, - long chunkStartTimeMs, long 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 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 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, - 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, availableRangeEndMs, chunkStartTimeMs, chunkEndTimeMs); - checkLiveEdgeLatencyWithTemplateAndLimitedTimeshift(startTimeMs, liveEdgeLatencyMs, - seekPositionMs, availableRangeStartMs, availableRangeEndMs, chunkStartTimeMs, - chunkEndTimeMs); - } - - private void checkSegmentRequestSequenceOnMultiPeriodLive(DashChunkSource chunkSource) { + private static void checkSegmentRequestSequenceOnMultiPeriodLive(DashChunkSource chunkSource) { List queue = new ArrayList<>(); ChunkOperationHolder out = new ChunkOperationHolder(); @@ -630,7 +408,7 @@ public class DashChunkSourceTest extends InstrumentationTestCase { long chunkEndTimeMs = 1000; // request first chunk - checkLiveEdgeLatency(chunkSource, queue, out, seekPositionMs, + checkLiveEdgeConsistency(chunkSource, queue, out, seekPositionMs, availableRangeStartMs, availableRangeEndMs, chunkStartTimeMs, chunkEndTimeMs); queue.add((MediaChunk) out.chunk); @@ -638,7 +416,7 @@ public class DashChunkSourceTest extends InstrumentationTestCase { chunkStartTimeMs += 1000; chunkEndTimeMs += 1000; out.chunk = null; - checkLiveEdgeLatency(chunkSource, queue, out, seekPositionMs, + checkLiveEdgeConsistency(chunkSource, queue, out, seekPositionMs, availableRangeStartMs, availableRangeEndMs, chunkStartTimeMs, chunkEndTimeMs); queue.add((MediaChunk) out.chunk); @@ -646,7 +424,7 @@ public class DashChunkSourceTest extends InstrumentationTestCase { chunkStartTimeMs += 1000; chunkEndTimeMs += 1000; out.chunk = null; - checkLiveEdgeLatency(chunkSource, queue, out, seekPositionMs, + checkLiveEdgeConsistency(chunkSource, queue, out, seekPositionMs, availableRangeStartMs, availableRangeEndMs, chunkStartTimeMs, chunkEndTimeMs); queue.add((MediaChunk) out.chunk); @@ -654,7 +432,7 @@ public class DashChunkSourceTest extends InstrumentationTestCase { chunkStartTimeMs += 1000; chunkEndTimeMs += 1000; out.chunk = null; - checkLiveEdgeLatency(chunkSource, queue, out, seekPositionMs, + checkLiveEdgeConsistency(chunkSource, queue, out, seekPositionMs, availableRangeStartMs, availableRangeEndMs, chunkStartTimeMs, chunkEndTimeMs); queue.add((MediaChunk) out.chunk); @@ -662,7 +440,7 @@ public class DashChunkSourceTest extends InstrumentationTestCase { chunkStartTimeMs += 1000; chunkEndTimeMs += 1000; out.chunk = null; - checkLiveEdgeLatency(chunkSource, queue, out, seekPositionMs, + checkLiveEdgeConsistency(chunkSource, queue, out, seekPositionMs, availableRangeStartMs, availableRangeEndMs, chunkStartTimeMs, chunkEndTimeMs); queue.add((MediaChunk) out.chunk); @@ -670,7 +448,7 @@ public class DashChunkSourceTest extends InstrumentationTestCase { chunkStartTimeMs += 1000; chunkEndTimeMs += 1000; out.chunk = null; - checkLiveEdgeLatency(chunkSource, queue, out, seekPositionMs, + checkLiveEdgeConsistency(chunkSource, queue, out, seekPositionMs, availableRangeStartMs, availableRangeEndMs, chunkStartTimeMs, chunkEndTimeMs); queue.add((MediaChunk) out.chunk); @@ -678,7 +456,7 @@ public class DashChunkSourceTest extends InstrumentationTestCase { chunkStartTimeMs += 1000; chunkEndTimeMs += 1000; out.chunk = null; - checkLiveEdgeLatency(chunkSource, queue, out, seekPositionMs, + checkLiveEdgeConsistency(chunkSource, queue, out, seekPositionMs, availableRangeStartMs, availableRangeEndMs, chunkStartTimeMs, chunkEndTimeMs); queue.add((MediaChunk) out.chunk); @@ -686,7 +464,7 @@ public class DashChunkSourceTest extends InstrumentationTestCase { chunkStartTimeMs += 1000; chunkEndTimeMs += 1000; out.chunk = null; - checkLiveEdgeLatency(chunkSource, queue, out, seekPositionMs, + checkLiveEdgeConsistency(chunkSource, queue, out, seekPositionMs, availableRangeStartMs, availableRangeEndMs, chunkStartTimeMs, chunkEndTimeMs); queue.add((MediaChunk) out.chunk); @@ -694,7 +472,7 @@ public class DashChunkSourceTest extends InstrumentationTestCase { chunkStartTimeMs += 1000; chunkEndTimeMs += 1000; out.chunk = null; - checkLiveEdgeLatency(chunkSource, queue, out, seekPositionMs, + checkLiveEdgeConsistency(chunkSource, queue, out, seekPositionMs, availableRangeStartMs, availableRangeEndMs, chunkStartTimeMs, chunkEndTimeMs); queue.add((MediaChunk) out.chunk); @@ -702,7 +480,7 @@ public class DashChunkSourceTest extends InstrumentationTestCase { chunkStartTimeMs += 1000; chunkEndTimeMs += 1000; out.chunk = null; - checkLiveEdgeLatency(chunkSource, queue, out, seekPositionMs, + checkLiveEdgeConsistency(chunkSource, queue, out, seekPositionMs, availableRangeStartMs, availableRangeEndMs, chunkStartTimeMs, chunkEndTimeMs); queue.add((MediaChunk) out.chunk); diff --git a/library/src/androidTest/java/com/google/android/exoplayer/dash/mpd/RepresentationTest.java b/library/src/androidTest/java/com/google/android/exoplayer/dash/mpd/RepresentationTest.java index dc0ca5047b..03f987cff6 100644 --- a/library/src/androidTest/java/com/google/android/exoplayer/dash/mpd/RepresentationTest.java +++ b/library/src/androidTest/java/com/google/android/exoplayer/dash/mpd/RepresentationTest.java @@ -30,12 +30,11 @@ public class RepresentationTest extends TestCase { String uri = "http://www.google.com"; SegmentBase base = new SingleSegmentBase(new RangedUri(uri, null, 0, 1), 1, 0, uri, 1, 1); Format format = new Format("0", MimeTypes.VIDEO_MP4, 1920, 1080, -1, 0, 0, 2500000); - Representation representation = Representation.newInstance(-1, -1, "test_stream_1", 3, - format, base); + Representation representation = Representation.newInstance("test_stream_1", 3, format, base); assertEquals("test_stream_1.0.3", representation.getCacheKey()); format = new Format("150", MimeTypes.VIDEO_MP4, 1920, 1080, -1, 0, 0, 2500000); - representation = Representation.newInstance(-1, -1, "test_stream_1", -1, format, base); + representation = Representation.newInstance("test_stream_1", -1, format, base); assertEquals("test_stream_1.150.-1", representation.getCacheKey()); } 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 952f0c1a48..5395b86d16 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 @@ -16,11 +16,11 @@ package com.google.android.exoplayer.dash; import com.google.android.exoplayer.BehindLiveWindowException; +import com.google.android.exoplayer.C; import com.google.android.exoplayer.MediaFormat; import com.google.android.exoplayer.TimeRange; import com.google.android.exoplayer.TimeRange.DynamicTimeRange; import com.google.android.exoplayer.TimeRange.StaticTimeRange; -import com.google.android.exoplayer.TrackRenderer; import com.google.android.exoplayer.chunk.Chunk; import com.google.android.exoplayer.chunk.ChunkExtractorWrapper; import com.google.android.exoplayer.chunk.ChunkOperationHolder; @@ -141,11 +141,13 @@ public class DashChunkSource implements ChunkSource { * * @param dataSource A {@link DataSource} suitable for loading the media data. * @param formatEvaluator Selects from the available formats. + * @param durationMs The duration of the content. * @param representations The representations to be considered by the source. */ public DashChunkSource(DataSource dataSource, FormatEvaluator formatEvaluator, - Representation... representations) { - this(buildManifest(Arrays.asList(representations)), 0, null, dataSource, formatEvaluator); + long durationMs, Representation... representations) { + this(buildManifest(durationMs, Arrays.asList(representations)), 0, null, dataSource, + formatEvaluator); } /** @@ -153,11 +155,12 @@ public class DashChunkSource implements ChunkSource { * * @param dataSource A {@link DataSource} suitable for loading the media data. * @param formatEvaluator Selects from the available formats. + * @param durationMs The duration of the content. * @param representations The representations to be considered by the source. */ public DashChunkSource(DataSource dataSource, FormatEvaluator formatEvaluator, - List representations) { - this(buildManifest(representations), 0, null, dataSource, formatEvaluator); + long durationMs, List representations) { + this(buildManifest(durationMs, representations), 0, null, dataSource, formatEvaluator); } /** @@ -277,9 +280,9 @@ public class DashChunkSource implements ChunkSource { String mimeType = ""; 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; + if (totalDurationUs != C.UNKNOWN_TIME_US) { + if (periodHolder.durationUs == C.UNKNOWN_TIME_US) { + totalDurationUs = C.UNKNOWN_TIME_US; } else { totalDurationUs += periodHolder.durationUs; } @@ -394,9 +397,7 @@ 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; - long segmentStartTimeUs; - int segmentNum = -1; - boolean startingNewPeriod = false; + boolean startingNewPeriod; PeriodHolder periodHolder; availableRange.getCurrentBoundsUs(availableRangeValues); @@ -415,7 +416,6 @@ public class DashChunkSource implements ChunkSource { } periodHolder = findPeriodHolder(seekPositionUs); - segmentStartTimeUs = seekPositionUs; startingNewPeriod = true; } else { if (startAtLiveEdge) { @@ -430,15 +430,13 @@ public class DashChunkSource implements ChunkSource { return; } - segmentNum = previous.chunkIndex + 1; - segmentStartTimeUs = previous.endTimeUs; - if (currentManifest.dynamic) { - if (segmentStartTimeUs < availableRangeValues[0]) { + long nextSegmentStartTimeUs = previous.endTimeUs; + if (nextSegmentStartTimeUs < availableRangeValues[0]) { // This is before the first chunk in the current manifest. fatalError = new BehindLiveWindowException(); return; - } else if (segmentStartTimeUs >= availableRangeValues[1]) { + } else if (nextSegmentStartTimeUs >= 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. @@ -446,21 +444,23 @@ public class DashChunkSource implements ChunkSource { } } + startingNewPeriod = false; periodHolder = periodHolders.get(previous.parentId); if (periodHolder == null) { - // the previous chunk was from a period that's no longer on the manifest, therefore the + // 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) + // 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; + } else if (!periodHolder.isIndexUnbounded()) { + RepresentationHolder representationHolder = + periodHolder.representationHolders.get(previous.format.id); + if (representationHolder.isLastSegment(previous.chunkIndex)) { + // We reached the end of a period. Start the next one. + periodHolder = periodHolders.get(previous.parentId + 1); + startingNewPeriod = true; + } } } @@ -483,24 +483,16 @@ public class DashChunkSource implements ChunkSource { if (pendingInitializationUri != null || pendingIndexUri != null) { // We have initialization and/or index requests to make. Chunk initializationChunk = newInitializationChunk(pendingInitializationUri, pendingIndexUri, - selectedRepresentation, extractorWrapper, dataSource, periodHolder.manifestIndex, + selectedRepresentation, extractorWrapper, dataSource, periodHolder.localIndex, evaluation.trigger); lastChunkWasInitialization = true; out.chunk = initializationChunk; return; } - 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(); - } - } - + int segmentNum = queue.isEmpty() ? representationHolder.getSegmentNum(seekPositionUs) + : startingNewPeriod ? representationHolder.getFirstAvailableSegmentNum() + : queue.get(out.queueSize - 1).chunkIndex + 1; Chunk nextMediaChunk = newMediaChunk(periodHolder, representationHolder, dataSource, mediaFormat, segmentNum, evaluation.trigger); lastChunkWasInitialization = false; @@ -534,8 +526,7 @@ public class DashChunkSource implements ChunkSource { if (initializationChunk.hasSeekMap()) { representationHolder.segmentIndex = new DashWrappingSegmentIndex( (ChunkIndex) initializationChunk.getSeekMap(), - initializationChunk.dataSpec.uri.toString(), - periodHolder.startTimeUs); + initializationChunk.dataSpec.uri.toString()); } // The null check avoids overwriting drmInitData obtained from the manifest with drmInitData @@ -594,13 +585,13 @@ public class DashChunkSource implements ChunkSource { return new SingleSampleMediaChunk(dataSource, dataSpec, Chunk.TRIGGER_INITIAL, representation.format, startTimeUs, endTimeUs, segmentNum, isLastSegment, MediaFormat.createTextFormat(MimeTypes.TEXT_VTT, MediaFormat.NO_VALUE, - representation.format.language), null, periodHolder.manifestIndex); + representation.format.language), null, periodHolder.localIndex); } else { boolean isMediaFormatFinal = (mediaFormat != null); return new ContainerMediaChunk(dataSource, dataSpec, trigger, representation.format, startTimeUs, endTimeUs, segmentNum, isLastSegment, sampleOffsetUs, representationHolder.extractorWrapper, mediaFormat, maxWidth, maxHeight, drmInitData, - isMediaFormatFinal, periodHolder.manifestIndex); + isMediaFormatFinal, periodHolder.localIndex); } } @@ -624,7 +615,7 @@ public class DashChunkSource implements ChunkSource { private static DrmInitData getDrmInitData(MediaPresentationDescription manifest, int adaptationSetIndex) { - AdaptationSet adaptationSet = manifest.periods.get(0).adaptationSets.get(adaptationSetIndex); + AdaptationSet adaptationSet = manifest.getPeriod(0).adaptationSets.get(adaptationSetIndex); String drmInitMimeType = mimeTypeIsWebm(adaptationSet.representations.get(0).format.mimeType) ? MimeTypes.VIDEO_WEBM : MimeTypes.VIDEO_MP4; if (adaptationSet.contentProtections.isEmpty()) { @@ -644,13 +635,11 @@ public class DashChunkSource implements ChunkSource { } } - private static MediaPresentationDescription buildManifest(List representations) { - Representation firstRepresentation = representations.get(0); + private static MediaPresentationDescription buildManifest(long durationMs, + List representations) { AdaptationSet adaptationSet = new AdaptationSet(0, AdaptationSet.TYPE_UNKNOWN, representations); - Period period = new Period(null, firstRepresentation.periodStartMs, - firstRepresentation.periodDurationMs, Collections.singletonList(adaptationSet)); - long duration = firstRepresentation.periodDurationMs - firstRepresentation.periodStartMs; - return new MediaPresentationDescription(-1, duration, -1, false, -1, -1, null, null, + Period period = new Period(null, 0, Collections.singletonList(adaptationSet)); + return new MediaPresentationDescription(-1, durationMs, -1, false, -1, -1, null, null, Collections.singletonList(period)); } @@ -672,21 +661,19 @@ public class DashChunkSource implements ChunkSource { } private void processManifest(MediaPresentationDescription manifest) { - List newPeriods = manifest.periods; - // Remove old periods. - Period firstPeriod = newPeriods.get(0); + Period firstPeriod = manifest.getPeriod(0); while (periodHolders.size() > 0 && periodHolders.valueAt(0).startTimeUs < firstPeriod.startMs * 1000) { PeriodHolder periodHolder = periodHolders.valueAt(0); // TODO: Use periodHolders.removeAt(0) if the minimum API level is ever increased to 11. - periodHolders.remove(periodHolder.manifestIndex); + periodHolders.remove(periodHolder.localIndex); } // Update existing periods. try { for (int i = 0; i < periodHolders.size(); i++) { - periodHolders.valueAt(i).updatePeriod(newPeriods.get(i)); + periodHolders.valueAt(i).updatePeriod(manifest, i); } } catch (BehindLiveWindowException e) { fatalError = e; @@ -694,9 +681,8 @@ public class DashChunkSource implements ChunkSource { } // Add new periods. - for (int i = periodHolders.size(); i < newPeriods.size(); i++) { - Period period = newPeriods.get(i); - PeriodHolder periodHolder = new PeriodHolder(periodHolderNextIndex, period, + for (int i = periodHolders.size(); i < manifest.getPeriodCount(); i++) { + PeriodHolder periodHolder = new PeriodHolder(periodHolderNextIndex, manifest, i, adaptationSetIndex, representationIndices); periodHolders.put(periodHolderNextIndex, periodHolder); periodHolderNextIndex++; @@ -725,7 +711,7 @@ public class DashChunkSource implements ChunkSource { long maxEndPositionUs = lastPeriod.isIndexUnbounded() ? Long.MAX_VALUE : lastPeriod.getAvailableEndTimeUs(); long elapsedRealtimeAtZeroUs = (systemClock.elapsedRealtime() * 1000) - - (nowUs - currentManifest.availabilityStartTime * 1000); + - (nowUs - (currentManifest.availabilityStartTime * 1000)); long timeShiftBufferDepthUs = currentManifest.timeShiftBufferDepth == -1 ? -1 : currentManifest.timeShiftBufferDepth * 1000; return new DynamicTimeRange(minStartPositionUs, maxEndPositionUs, elapsedRealtimeAtZeroUs, @@ -751,58 +737,76 @@ public class DashChunkSource implements ChunkSource { public DashSegmentIndex segmentIndex; public MediaFormat mediaFormat; + private final long periodStartTimeUs; + private long periodDurationUs; + private int segmentNumShift; - public RepresentationHolder(Representation representation, - ChunkExtractorWrapper extractorWrapper) { + public RepresentationHolder(long periodStartTimeUs, long periodDurationUs, + Representation representation, ChunkExtractorWrapper extractorWrapper) { + this.periodStartTimeUs = periodStartTimeUs; + this.periodDurationUs = periodDurationUs; this.representation = representation; this.extractorWrapper = extractorWrapper; - this.segmentIndex = representation.getIndex(); + segmentIndex = representation.getIndex(); } - public void updateRepresentation(Representation newRepresentation) + public void updateRepresentation(long newPeriodDurationUs, Representation newRepresentation) throws BehindLiveWindowException{ DashSegmentIndex oldIndex = representation.getIndex(); DashSegmentIndex newIndex = newRepresentation.getIndex(); + + periodDurationUs = newPeriodDurationUs; representation = newRepresentation; - if (newIndex == null) { + if (oldIndex == null) { + // Segment numbers cannot shift if the index isn't defined by the manifest. return; } segmentIndex = newIndex; - int oldIndexLastSegmentNum = oldIndex.getLastSegmentNum(); + if (!oldIndex.isExplicit()) { + // Segment numbers cannot shift if the index isn't explicit. + return; + } + + int oldIndexLastSegmentNum = oldIndex.getLastSegmentNum(periodDurationUs); long oldIndexEndTimeUs = oldIndex.getTimeUs(oldIndexLastSegmentNum) - + oldIndex.getDurationUs(oldIndexLastSegmentNum); + + oldIndex.getDurationUs(oldIndexLastSegmentNum, periodDurationUs); int newIndexFirstSegmentNum = newIndex.getFirstSegmentNum(); long newIndexStartTimeUs = newIndex.getTimeUs(newIndexFirstSegmentNum); if (oldIndexEndTimeUs == newIndexStartTimeUs) { // The new manifest continues where the old one ended, with no overlap. - segmentNumShift += oldIndex.getLastSegmentNum() + 1 - newIndexFirstSegmentNum; + segmentNumShift += oldIndex.getLastSegmentNum(periodDurationUs) + 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; + segmentNumShift += oldIndex.getSegmentNum(newIndexStartTimeUs, periodDurationUs) + - newIndexFirstSegmentNum; } } public int getSegmentNum(long positionUs) { - return segmentIndex.getSegmentNum(positionUs) + segmentNumShift; + return segmentIndex.getSegmentNum(positionUs - periodStartTimeUs, periodDurationUs) + + segmentNumShift; } public long getSegmentStartTimeUs(int segmentNum) { - return segmentIndex.getTimeUs(segmentNum - segmentNumShift); + return segmentIndex.getTimeUs(segmentNum - segmentNumShift) + periodStartTimeUs; } public long getSegmentEndTimeUs(int segmentNum) { return getSegmentStartTimeUs(segmentNum) - + segmentIndex.getDurationUs(segmentNum - segmentNumShift); + + segmentIndex.getDurationUs(segmentNum - segmentNumShift, periodDurationUs); } public boolean isLastSegment(int segmentNum) { - return (segmentNum - segmentNumShift) == segmentIndex.getLastSegmentNum(); + int lastSegmentNum = segmentIndex.getLastSegmentNum(periodDurationUs); + return lastSegmentNum == DashSegmentIndex.INDEX_UNBOUNDED ? false + : segmentNum == (lastSegmentNum + segmentNumShift); } public int getFirstAvailableSegmentNum() { @@ -817,7 +821,7 @@ public class DashChunkSource implements ChunkSource { private static final class PeriodHolder { - public final int manifestIndex; + public final int localIndex; public final long startTimeUs; public final long durationUs; public final String mimeType; @@ -834,12 +838,16 @@ public class DashChunkSource implements ChunkSource { private long availableStartTimeUs; private long availableEndTimeUs; - public PeriodHolder(int manifestIndex, Period period, int adaptationSetIndex, - int[] representationIndices) { - this.manifestIndex = manifestIndex; + public PeriodHolder(int localIndex, MediaPresentationDescription manifest, + int manifestIndex, int adaptationSetIndex, int[] representationIndices) { + this.localIndex = localIndex; this.adaptationSetIndex = adaptationSetIndex; this.representationIndices = representationIndices; + Period period = manifest.getPeriod(manifestIndex); + startTimeUs = period.startMs * 1000; + durationUs = getPeriodDurationUs(manifest, manifestIndex); + List periodRepresentations = period.adaptationSets.get(adaptationSetIndex).representations; int representationCount = representationIndices != null ? representationIndices.length @@ -858,35 +866,32 @@ public class DashChunkSource implements ChunkSource { 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)); + : new FragmentedMp4Extractor(); + RepresentationHolder representationHolder = new RepresentationHolder(startTimeUs, + durationUs, representation, new ChunkExtractorWrapper(extractor)); representationHolders.put(formats[i].id, representationHolder); } this.maxWidth = maxWidth; this.maxHeight = maxHeight; this.mimeType = mimeType; - startTimeUs = period.startMs * 1000; - long durationMs = period.durationMs; - if (durationMs == -1) { - durationUs = TrackRenderer.UNKNOWN_TIME_US; - } else { - durationUs = durationMs * 1000; - } - Arrays.sort(formats, new DecreasingBandwidthComparator()); updateRepresentationIndependentProperties(); } - public void updatePeriod(Period period) throws BehindLiveWindowException { + public void updatePeriod(MediaPresentationDescription manifest, int manifestIndex) + throws BehindLiveWindowException { + Period period = manifest.getPeriod(manifestIndex); + long durationUs = getPeriodDurationUs(manifest, manifestIndex); + List representations = period.adaptationSets.get(adaptationSetIndex).representations; int representationCount = formats.length; for (int i = 0; i < representationCount; i++) { int representationIndex = representationIndices != null ? representationIndices[i] : i; Representation representation = representations.get(representationIndex); - representationHolders.get(representation.format.id).updateRepresentation(representation); + representationHolders.get(representation.format.id).updateRepresentation(durationUs, + representation); } updateRepresentationIndependentProperties(); } @@ -915,13 +920,14 @@ public class DashChunkSource implements ChunkSource { Representation representation = representationHolders.get(formats[0].id).representation; DashSegmentIndex segmentIndex = representation.getIndex(); if (segmentIndex != null) { - int lastSegmentNum = segmentIndex.getLastSegmentNum(); + int firstSegmentNum = segmentIndex.getFirstSegmentNum(); + int lastSegmentNum = segmentIndex.getLastSegmentNum(durationUs); indexIsUnbounded = lastSegmentNum == DashSegmentIndex.INDEX_UNBOUNDED; indexIsExplicit = segmentIndex.isExplicit(); - availableStartTimeUs = segmentIndex.getTimeUs(segmentIndex.getFirstSegmentNum()); + availableStartTimeUs = startTimeUs + segmentIndex.getTimeUs(firstSegmentNum); if (!indexIsUnbounded) { - availableEndTimeUs = segmentIndex.getTimeUs(lastSegmentNum) - + segmentIndex.getDurationUs(lastSegmentNum); + availableEndTimeUs = startTimeUs + segmentIndex.getTimeUs(lastSegmentNum) + + segmentIndex.getDurationUs(lastSegmentNum, durationUs); } } else { indexIsUnbounded = false; @@ -931,6 +937,15 @@ public class DashChunkSource implements ChunkSource { } } + private static long getPeriodDurationUs(MediaPresentationDescription manifest, int index) { + long durationMs = manifest.getPeriodDuration(index); + if (durationMs == -1) { + return C.UNKNOWN_TIME_US; + } else { + return durationMs * 1000; + } + } + } } diff --git a/library/src/main/java/com/google/android/exoplayer/dash/DashSegmentIndex.java b/library/src/main/java/com/google/android/exoplayer/dash/DashSegmentIndex.java index e922757e13..95de1cd0d9 100644 --- a/library/src/main/java/com/google/android/exoplayer/dash/DashSegmentIndex.java +++ b/library/src/main/java/com/google/android/exoplayer/dash/DashSegmentIndex.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer.dash; +import com.google.android.exoplayer.C; import com.google.android.exoplayer.dash.mpd.RangedUri; /** @@ -24,20 +25,22 @@ import com.google.android.exoplayer.dash.mpd.RangedUri; */ public interface DashSegmentIndex { - public static final int INDEX_UNBOUNDED = -1; + int INDEX_UNBOUNDED = -1; /** * Returns the segment number of the segment containing a given media time. *

* If the given media time is outside the range of the index, then the returned segment number is * clamped to {@link #getFirstSegmentNum()} (if the given media time is earlier the start of the - * first segment) or {@link #getLastSegmentNum()} (if the given media time is later then the end - * of the last segment). + * first segment) or {@link #getLastSegmentNum(long)} (if the given media time is later then the + * end of the last segment). * * @param timeUs The time in microseconds. + * @param periodDurationUs The duration of the enclosing period in microseconds, or + * {@link C#UNKNOWN_TIME_US} if the period's duration is not yet known. * @return The segment number of the corresponding segment. */ - int getSegmentNum(long timeUs); + int getSegmentNum(long timeUs, long periodDurationUs); /** * Returns the start time of a segment. @@ -51,9 +54,11 @@ public interface DashSegmentIndex { * Returns the duration of a segment. * * @param segmentNum The segment number. + * @param periodDurationUs The duration of the enclosing period in microseconds, or + * {@link C#UNKNOWN_TIME_US} if the period's duration is not yet known. * @return The duration of the segment, in microseconds. */ - long getDurationUs(int segmentNum); + long getDurationUs(int segmentNum, long periodDurationUs); /** * Returns a {@link RangedUri} defining the location of a segment. @@ -73,15 +78,15 @@ public interface DashSegmentIndex { /** * Returns the segment number of the last segment, or {@link #INDEX_UNBOUNDED}. *

- * An unbounded index occurs if a live stream manifest uses SegmentTemplate elements without a - * SegmentTimeline element. In this case the manifest can be used to derive information about - * segments arbitrarily far into the future. This means that the manifest does not need to be - * refreshed as frequently (if at all) during playback, however it is necessary for a player to - * manually calculate the window of currently available segments. + * An unbounded index occurs if a dynamic manifest uses SegmentTemplate elements without a + * SegmentTimeline element, and if the period duration is not yet known. In this case the caller + * must manually determine the window of currently available segments. * + * @param periodDurationUs The duration of the enclosing period in microseconds, or + * {@link C#UNKNOWN_TIME_US} if the period's duration is not yet known. * @return The segment number of the last segment, or {@link #INDEX_UNBOUNDED}. */ - int getLastSegmentNum(); + int getLastSegmentNum(long periodDurationUs); /** * Returns true if segments are defined explicitly by the index. diff --git a/library/src/main/java/com/google/android/exoplayer/dash/DashWrappingSegmentIndex.java b/library/src/main/java/com/google/android/exoplayer/dash/DashWrappingSegmentIndex.java index bcff061458..5542232e08 100644 --- a/library/src/main/java/com/google/android/exoplayer/dash/DashWrappingSegmentIndex.java +++ b/library/src/main/java/com/google/android/exoplayer/dash/DashWrappingSegmentIndex.java @@ -26,17 +26,14 @@ import com.google.android.exoplayer.extractor.ChunkIndex; private final ChunkIndex chunkIndex; private final String uri; - private final long startTimeUs; /** * @param chunkIndex The {@link ChunkIndex} to wrap. * @param uri The URI where the data is located. - * @param startTimeUs The start time of the index, in microseconds. */ - public DashWrappingSegmentIndex(ChunkIndex chunkIndex, String uri, long startTimeUs) { + public DashWrappingSegmentIndex(ChunkIndex chunkIndex, String uri) { this.chunkIndex = chunkIndex; this.uri = uri; - this.startTimeUs = startTimeUs; } @Override @@ -45,17 +42,17 @@ import com.google.android.exoplayer.extractor.ChunkIndex; } @Override - public int getLastSegmentNum() { + public int getLastSegmentNum(long periodDurationUs) { return chunkIndex.length - 1; } @Override public long getTimeUs(int segmentNum) { - return chunkIndex.timesUs[segmentNum] + startTimeUs; + return chunkIndex.timesUs[segmentNum]; } @Override - public long getDurationUs(int segmentNum) { + public long getDurationUs(int segmentNum, long periodDurationUs) { return chunkIndex.durationsUs[segmentNum]; } @@ -65,8 +62,8 @@ import com.google.android.exoplayer.extractor.ChunkIndex; } @Override - public int getSegmentNum(long timeUs) { - return chunkIndex.getChunkIndex(timeUs - startTimeUs); + public int getSegmentNum(long timeUs, long periodDurationUs) { + return chunkIndex.getChunkIndex(timeUs); } @Override diff --git a/library/src/main/java/com/google/android/exoplayer/dash/mpd/DashSingleSegmentIndex.java b/library/src/main/java/com/google/android/exoplayer/dash/mpd/DashSingleSegmentIndex.java index 9b8f6b276c..fe8db6ffbd 100644 --- a/library/src/main/java/com/google/android/exoplayer/dash/mpd/DashSingleSegmentIndex.java +++ b/library/src/main/java/com/google/android/exoplayer/dash/mpd/DashSingleSegmentIndex.java @@ -22,34 +22,28 @@ import com.google.android.exoplayer.dash.DashSegmentIndex; */ /* package */ final class DashSingleSegmentIndex implements DashSegmentIndex { - private final long startTimeUs; - private final long durationUs; private final RangedUri uri; /** - * @param startTimeUs The start time of the segment, in microseconds. - * @param durationUs The duration of the segment, in microseconds. * @param uri A {@link RangedUri} defining the location of the segment data. */ - public DashSingleSegmentIndex(long startTimeUs, long durationUs, RangedUri uri) { - this.startTimeUs = startTimeUs; - this.durationUs = durationUs; + public DashSingleSegmentIndex(RangedUri uri) { this.uri = uri; } @Override - public int getSegmentNum(long timeUs) { + public int getSegmentNum(long timeUs, long periodDurationUs) { return 0; } @Override public long getTimeUs(int segmentNum) { - return startTimeUs; + return 0; } @Override - public long getDurationUs(int segmentNum) { - return durationUs; + public long getDurationUs(int segmentNum, long periodDurationUs) { + return periodDurationUs; } @Override @@ -63,7 +57,7 @@ import com.google.android.exoplayer.dash.DashSegmentIndex; } @Override - public int getLastSegmentNum() { + public int getLastSegmentNum(long periodDurationUs) { return 0; } diff --git a/library/src/main/java/com/google/android/exoplayer/dash/mpd/MediaPresentationDescription.java b/library/src/main/java/com/google/android/exoplayer/dash/mpd/MediaPresentationDescription.java index f3c2cf4ae2..4b0b94754d 100644 --- a/library/src/main/java/com/google/android/exoplayer/dash/mpd/MediaPresentationDescription.java +++ b/library/src/main/java/com/google/android/exoplayer/dash/mpd/MediaPresentationDescription.java @@ -41,7 +41,7 @@ public class MediaPresentationDescription implements RedirectingManifest { public final String location; - public final List periods; + private final List periods; public MediaPresentationDescription(long availabilityStartTime, long duration, long minBufferTime, boolean dynamic, long minUpdatePeriod, long timeShiftBufferDepth, UtcTimingElement utcTiming, @@ -54,12 +54,26 @@ public class MediaPresentationDescription implements RedirectingManifest { this.timeShiftBufferDepth = timeShiftBufferDepth; this.utcTiming = utcTiming; this.location = location; - this.periods = Collections.unmodifiableList(periods); + this.periods = periods == null ? Collections.emptyList() : periods; } @Override - public String getNextManifestUri() { + public final String getNextManifestUri() { return location; } + public final int getPeriodCount() { + return periods.size(); + } + + public final Period getPeriod(int index) { + return periods.get(index); + } + + public final long getPeriodDuration(int index) { + return index == periods.size() - 1 + ? (duration == -1 ? -1 : duration - periods.get(index).startMs) + : periods.get(index + 1).startMs - periods.get(index).startMs; + } + } diff --git a/library/src/main/java/com/google/android/exoplayer/dash/mpd/MediaPresentationDescriptionParser.java b/library/src/main/java/com/google/android/exoplayer/dash/mpd/MediaPresentationDescriptionParser.java index 89ae6f6753..1305db85ce 100644 --- a/library/src/main/java/com/google/android/exoplayer/dash/mpd/MediaPresentationDescriptionParser.java +++ b/library/src/main/java/com/google/android/exoplayer/dash/mpd/MediaPresentationDescriptionParser.java @@ -30,6 +30,7 @@ import com.google.android.exoplayer.util.Util; import android.text.TextUtils; import android.util.Base64; +import android.util.Pair; import org.xml.sax.helpers.DefaultHandler; import org.xmlpull.v1.XmlPullParser; @@ -114,16 +115,32 @@ public class MediaPresentationDescriptionParser extends DefaultHandler String location = null; List periods = new ArrayList<>(); + long nextPeriodStartMs = dynamic ? -1 : 0; + boolean seenEarlyAccessPeriod = false; do { xpp.next(); if (isStartTag(xpp, "BaseURL")) { baseUrl = parseBaseUrl(xpp, baseUrl); } else if (isStartTag(xpp, "UTCTiming")) { utcTiming = parseUtcTiming(xpp); - } else if (isStartTag(xpp, "Period")) { - periods.add(parsePeriod(xpp, baseUrl, durationMs)); } else if (isStartTag(xpp, "Location")) { location = xpp.nextText(); + } else if (isStartTag(xpp, "Period") && !seenEarlyAccessPeriod) { + Pair periodWithDurationMs = parsePeriod(xpp, baseUrl, nextPeriodStartMs); + Period period = periodWithDurationMs.first; + if (period.startMs == -1) { + if (dynamic) { + // This is an early access period. Ignore it. All subsequent periods must also be + // early access. + seenEarlyAccessPeriod = true; + } else { + throw new ParserException("Unable to determine start of period " + periods.size()); + } + } else { + long periodDurationMs = periodWithDurationMs.second; + nextPeriodStartMs = periodDurationMs == -1 ? -1 : period.startMs + periodDurationMs; + periods.add(period); + } } } while (!isEndTag(xpp, "MPD")); @@ -149,11 +166,11 @@ public class MediaPresentationDescriptionParser extends DefaultHandler return new UtcTimingElement(schemeIdUri, value); } - protected Period parsePeriod(XmlPullParser xpp, String baseUrl, long mpdDurationMs) + protected Pair parsePeriod(XmlPullParser xpp, String baseUrl, long defaultStartMs) throws XmlPullParserException, IOException { String id = xpp.getAttributeValue(null, "id"); - long startMs = parseDuration(xpp, "start", 0); - long durationMs = parseDuration(xpp, "duration", mpdDurationMs); + long startMs = parseDuration(xpp, "start", defaultStartMs); + long durationMs = parseDuration(xpp, "duration", -1); SegmentBase segmentBase = null; List adaptationSets = new ArrayList<>(); do { @@ -161,29 +178,27 @@ public class MediaPresentationDescriptionParser extends DefaultHandler if (isStartTag(xpp, "BaseURL")) { baseUrl = parseBaseUrl(xpp, baseUrl); } else if (isStartTag(xpp, "AdaptationSet")) { - adaptationSets.add(parseAdaptationSet(xpp, baseUrl, startMs, durationMs, - segmentBase)); + adaptationSets.add(parseAdaptationSet(xpp, baseUrl, segmentBase)); } else if (isStartTag(xpp, "SegmentBase")) { segmentBase = parseSegmentBase(xpp, baseUrl, null); } else if (isStartTag(xpp, "SegmentList")) { - segmentBase = parseSegmentList(xpp, baseUrl, null, durationMs); + segmentBase = parseSegmentList(xpp, baseUrl, null); } else if (isStartTag(xpp, "SegmentTemplate")) { - segmentBase = parseSegmentTemplate(xpp, baseUrl, null, durationMs); + segmentBase = parseSegmentTemplate(xpp, baseUrl, null); } } while (!isEndTag(xpp, "Period")); - return buildPeriod(id, startMs, durationMs, adaptationSets); + return Pair.create(buildPeriod(id, startMs, adaptationSets), durationMs); } - protected Period buildPeriod( - String id, long startMs, long durationMs, List adaptationSets) { - return new Period(id, startMs, durationMs, adaptationSets); + protected Period buildPeriod(String id, long startMs, List adaptationSets) { + return new Period(id, startMs, adaptationSets); } // AdaptationSet parsing. - protected AdaptationSet parseAdaptationSet(XmlPullParser xpp, String baseUrl, long periodStartMs, - long periodDurationMs, SegmentBase segmentBase) throws XmlPullParserException, IOException { + protected AdaptationSet parseAdaptationSet(XmlPullParser xpp, String baseUrl, + SegmentBase segmentBase) throws XmlPullParserException, IOException { int id = parseInt(xpp, "id", -1); int contentType = parseContentType(xpp); @@ -208,9 +223,9 @@ public class MediaPresentationDescriptionParser extends DefaultHandler language = checkLanguageConsistency(language, xpp.getAttributeValue(null, "lang")); contentType = checkContentTypeConsistency(contentType, parseContentType(xpp)); } else if (isStartTag(xpp, "Representation")) { - Representation representation = parseRepresentation(xpp, baseUrl, periodStartMs, - periodDurationMs, mimeType, codecs, width, height, frameRate, audioChannels, - audioSamplingRate, language, segmentBase, contentProtectionsBuilder); + Representation representation = parseRepresentation(xpp, baseUrl, mimeType, codecs, width, + height, frameRate, audioChannels, audioSamplingRate, language, segmentBase, + contentProtectionsBuilder); contentProtectionsBuilder.endRepresentation(); contentType = checkContentTypeConsistency(contentType, getContentType(representation)); representations.add(representation); @@ -219,10 +234,9 @@ public class MediaPresentationDescriptionParser extends DefaultHandler } else if (isStartTag(xpp, "SegmentBase")) { segmentBase = parseSegmentBase(xpp, baseUrl, (SingleSegmentBase) segmentBase); } else if (isStartTag(xpp, "SegmentList")) { - segmentBase = parseSegmentList(xpp, baseUrl, (SegmentList) segmentBase, periodDurationMs); + segmentBase = parseSegmentList(xpp, baseUrl, (SegmentList) segmentBase); } else if (isStartTag(xpp, "SegmentTemplate")) { - segmentBase = parseSegmentTemplate(xpp, baseUrl, (SegmentTemplate) segmentBase, - periodDurationMs); + segmentBase = parseSegmentTemplate(xpp, baseUrl, (SegmentTemplate) segmentBase); } else if (isStartTag(xpp)) { parseAdaptationSetChild(xpp); } @@ -311,9 +325,8 @@ public class MediaPresentationDescriptionParser extends DefaultHandler // Representation parsing. protected Representation parseRepresentation(XmlPullParser xpp, String baseUrl, - long periodStartMs, long periodDurationMs, String adaptationSetMimeType, - String adaptationSetCodecs, int adaptationSetWidth, int adaptationSetHeight, - float adaptationSetFrameRate, int adaptationSetAudioChannels, + String adaptationSetMimeType, String adaptationSetCodecs, int adaptationSetWidth, + int adaptationSetHeight, float adaptationSetFrameRate, int adaptationSetAudioChannels, int adaptationSetAudioSamplingRate, String adaptationSetLanguage, SegmentBase segmentBase, ContentProtectionsBuilder contentProtectionsBuilder) throws XmlPullParserException, IOException { @@ -338,10 +351,9 @@ public class MediaPresentationDescriptionParser extends DefaultHandler } else if (isStartTag(xpp, "SegmentBase")) { segmentBase = parseSegmentBase(xpp, baseUrl, (SingleSegmentBase) segmentBase); } else if (isStartTag(xpp, "SegmentList")) { - segmentBase = parseSegmentList(xpp, baseUrl, (SegmentList) segmentBase, periodDurationMs); + segmentBase = parseSegmentList(xpp, baseUrl, (SegmentList) segmentBase); } else if (isStartTag(xpp, "SegmentTemplate")) { - segmentBase = parseSegmentTemplate(xpp, baseUrl, (SegmentTemplate) segmentBase, - periodDurationMs); + segmentBase = parseSegmentTemplate(xpp, baseUrl, (SegmentTemplate) segmentBase); } else if (isStartTag(xpp, "ContentProtection")) { contentProtectionsBuilder.addRepresentationProtection(parseContentProtection(xpp)); } @@ -349,7 +361,7 @@ public class MediaPresentationDescriptionParser extends DefaultHandler Format format = buildFormat(id, mimeType, width, height, frameRate, audioChannels, audioSamplingRate, bandwidth, language, codecs); - return buildRepresentation(periodStartMs, periodDurationMs, contentId, -1, format, + return buildRepresentation(contentId, -1, format, segmentBase != null ? segmentBase : new SingleSegmentBase(baseUrl)); } @@ -359,10 +371,9 @@ public class MediaPresentationDescriptionParser extends DefaultHandler bandwidth, language, codecs); } - protected Representation buildRepresentation(long periodStartMs, long periodDurationMs, - String contentId, int revisionId, Format format, SegmentBase segmentBase) { - return Representation.newInstance(periodStartMs, periodDurationMs, contentId, revisionId, - format, segmentBase); + protected Representation buildRepresentation(String contentId, int revisionId, Format format, + SegmentBase segmentBase) { + return Representation.newInstance(contentId, revisionId, format, segmentBase); } // SegmentBase, SegmentList and SegmentTemplate parsing. @@ -401,8 +412,8 @@ public class MediaPresentationDescriptionParser extends DefaultHandler indexStart, indexLength); } - protected SegmentList parseSegmentList(XmlPullParser xpp, String baseUrl, SegmentList parent, - long periodDurationMs) throws XmlPullParserException, IOException { + protected SegmentList parseSegmentList(XmlPullParser xpp, String baseUrl, SegmentList parent) + throws XmlPullParserException, IOException { long timescale = parseLong(xpp, "timescale", parent != null ? parent.timescale : 1); long presentationTimeOffset = parseLong(xpp, "presentationTimeOffset", @@ -434,19 +445,19 @@ public class MediaPresentationDescriptionParser extends DefaultHandler segments = segments != null ? segments : parent.mediaSegments; } - return buildSegmentList(initialization, timescale, presentationTimeOffset, periodDurationMs, + return buildSegmentList(initialization, timescale, presentationTimeOffset, startNumber, duration, timeline, segments); } protected SegmentList buildSegmentList(RangedUri initialization, long timescale, - long presentationTimeOffset, long periodDurationMs, int startNumber, long duration, + long presentationTimeOffset, int startNumber, long duration, List timeline, List segments) { - return new SegmentList(initialization, timescale, presentationTimeOffset, periodDurationMs, + return new SegmentList(initialization, timescale, presentationTimeOffset, startNumber, duration, timeline, segments); } protected SegmentTemplate parseSegmentTemplate(XmlPullParser xpp, String baseUrl, - SegmentTemplate parent, long periodDurationMs) throws XmlPullParserException, IOException { + SegmentTemplate parent) throws XmlPullParserException, IOException { long timescale = parseLong(xpp, "timescale", parent != null ? parent.timescale : 1); long presentationTimeOffset = parseLong(xpp, "presentationTimeOffset", @@ -475,15 +486,15 @@ public class MediaPresentationDescriptionParser extends DefaultHandler timeline = timeline != null ? timeline : parent.segmentTimeline; } - return buildSegmentTemplate(initialization, timescale, presentationTimeOffset, periodDurationMs, + return buildSegmentTemplate(initialization, timescale, presentationTimeOffset, startNumber, duration, timeline, initializationTemplate, mediaTemplate, baseUrl); } protected SegmentTemplate buildSegmentTemplate(RangedUri initialization, long timescale, - long presentationTimeOffset, long periodDurationMs, int startNumber, long duration, + long presentationTimeOffset, int startNumber, long duration, List timeline, UrlTemplate initializationTemplate, UrlTemplate mediaTemplate, String baseUrl) { - return new SegmentTemplate(initialization, timescale, presentationTimeOffset, periodDurationMs, + return new SegmentTemplate(initialization, timescale, presentationTimeOffset, startNumber, duration, timeline, initializationTemplate, mediaTemplate, baseUrl); } diff --git a/library/src/main/java/com/google/android/exoplayer/dash/mpd/Period.java b/library/src/main/java/com/google/android/exoplayer/dash/mpd/Period.java index c1cc738661..f8398fdd3e 100644 --- a/library/src/main/java/com/google/android/exoplayer/dash/mpd/Period.java +++ b/library/src/main/java/com/google/android/exoplayer/dash/mpd/Period.java @@ -33,11 +33,6 @@ public class Period { */ public final long startMs; - /** - * The duration of the period in milliseconds, or -1 if the duration is unknown. - */ - public final long durationMs; - /** * The adaptation sets belonging to the period. */ @@ -46,13 +41,11 @@ public class Period { /** * @param id The period identifier. May be null. * @param start The start time of the period in milliseconds. - * @param duration The duration of the period in milliseconds, or -1 if the duration is unknown. * @param adaptationSets The adaptation sets belonging to the period. */ - public Period(String id, long start, long duration, List adaptationSets) { + public Period(String id, long start, List adaptationSets) { this.id = id; this.startMs = start; - this.durationMs = duration; this.adaptationSets = Collections.unmodifiableList(adaptationSets); } diff --git a/library/src/main/java/com/google/android/exoplayer/dash/mpd/Representation.java b/library/src/main/java/com/google/android/exoplayer/dash/mpd/Representation.java index 54c0d6913a..8c8778f3e2 100644 --- a/library/src/main/java/com/google/android/exoplayer/dash/mpd/Representation.java +++ b/library/src/main/java/com/google/android/exoplayer/dash/mpd/Representation.java @@ -35,7 +35,6 @@ public abstract class Representation implements FormatWrapper { * {@link #contentId}, which should uniquely identify that video. */ public final String contentId; - /** * Identifies the revision of the content. *

@@ -45,22 +44,10 @@ public abstract class Representation implements FormatWrapper { * timestamp at which the media was encoded is often a suitable. */ public final long revisionId; - /** * The format of the representation. */ public final Format format; - - /** - * The start time of the enclosing period in milliseconds since the epoch. - */ - public final long periodStartMs; - - /** - * The duration of the enclosing period in milliseconds. - */ - public final long periodDurationMs; - /** * The offset of the presentation timestamps in the media stream relative to media time. */ @@ -71,33 +58,28 @@ public abstract class Representation implements FormatWrapper { /** * Constructs a new instance. * - * @param periodStartMs The start time of the enclosing period in milliseconds. - * @param periodDurationMs The duration of the enclosing period in milliseconds, or -1 if the - * duration is unknown. * @param contentId Identifies the piece of content to which this representation belongs. * @param revisionId Identifies the revision of the content. * @param format The format of the representation. * @param segmentBase A segment base element for the representation. * @return The constructed instance. */ - public static Representation newInstance(long periodStartMs, long periodDurationMs, - String contentId, long revisionId, Format format, SegmentBase segmentBase) { + public static Representation newInstance(String contentId, long revisionId, Format format, + SegmentBase segmentBase) { if (segmentBase instanceof SingleSegmentBase) { - return new SingleSegmentRepresentation(periodStartMs, periodDurationMs, contentId, revisionId, - format, (SingleSegmentBase) segmentBase, -1); + return new SingleSegmentRepresentation(contentId, revisionId, format, + (SingleSegmentBase) segmentBase, -1); } else if (segmentBase instanceof MultiSegmentBase) { - return new MultiSegmentRepresentation(periodStartMs, periodDurationMs, contentId, revisionId, - format, (MultiSegmentBase) segmentBase); + return new MultiSegmentRepresentation(contentId, revisionId, format, + (MultiSegmentBase) segmentBase); } else { throw new IllegalArgumentException("segmentBase must be of type SingleSegmentBase or " + "MultiSegmentBase"); } } - private Representation(long periodStartMs, long periodDurationMs, String contentId, - long revisionId, Format format, SegmentBase segmentBase) { - this.periodStartMs = periodStartMs; - this.periodDurationMs = periodDurationMs; + private Representation(String contentId, long revisionId, Format format, + SegmentBase segmentBase) { this.contentId = contentId; this.revisionId = revisionId; this.format = format; @@ -165,9 +147,6 @@ public abstract class Representation implements FormatWrapper { private final DashSingleSegmentIndex segmentIndex; /** - * @param periodStartMs The start time of the enclosing period in milliseconds. - * @param periodDurationMs The duration of the enclosing period in milliseconds, or -1 if the - * duration is unknown. * @param contentId Identifies the piece of content to which this representation belongs. * @param revisionId Identifies the revision of the content. * @param format The format of the representation. @@ -178,37 +157,34 @@ public abstract class Representation implements FormatWrapper { * @param indexEnd The offset of the last byte of index data. * @param contentLength The content length, or -1 if unknown. */ - public static SingleSegmentRepresentation newInstance(long periodStartMs, long periodDurationMs, - String contentId, long revisionId, Format format, String uri, long initializationStart, - long initializationEnd, long indexStart, long indexEnd, long contentLength) { + public static SingleSegmentRepresentation newInstance(String contentId, long revisionId, + Format format, String uri, long initializationStart, long initializationEnd, + long indexStart, long indexEnd, long contentLength) { RangedUri rangedUri = new RangedUri(uri, null, initializationStart, initializationEnd - initializationStart + 1); SingleSegmentBase segmentBase = new SingleSegmentBase(rangedUri, 1, 0, uri, indexStart, indexEnd - indexStart + 1); - return new SingleSegmentRepresentation(periodStartMs, periodDurationMs, contentId, revisionId, + return new SingleSegmentRepresentation(contentId, revisionId, format, segmentBase, contentLength); } /** - * @param periodStartMs The start time of the enclosing period in milliseconds. - * @param periodDurationMs The duration of the enclosing period in milliseconds, or -1 if the - * duration is unknown. * @param contentId Identifies the piece of content to which this representation belongs. * @param revisionId Identifies the revision of the content. * @param format The format of the representation. * @param segmentBase The segment base underlying the representation. * @param contentLength The content length, or -1 if unknown. */ - public SingleSegmentRepresentation(long periodStartMs, long periodDurationMs, String contentId, - long revisionId, Format format, SingleSegmentBase segmentBase, long contentLength) { - super(periodStartMs, periodDurationMs, contentId, revisionId, format, segmentBase); + public SingleSegmentRepresentation(String contentId, long revisionId, Format format, + SingleSegmentBase segmentBase, long contentLength) { + super(contentId, revisionId, format, segmentBase); this.uri = Uri.parse(segmentBase.uri); this.indexUri = segmentBase.getIndex(); this.contentLength = contentLength; // If we have an index uri then the index is defined externally, and we shouldn't return one // directly. If we don't, then we can't do better than an index defining a single segment. - segmentIndex = indexUri != null ? null : new DashSingleSegmentIndex(periodStartMs * 1000, - periodDurationMs * 1000, new RangedUri(segmentBase.uri, null, 0, -1)); + segmentIndex = indexUri != null ? null + : new DashSingleSegmentIndex(new RangedUri(segmentBase.uri, null, 0, contentLength)); } @Override @@ -232,17 +208,14 @@ public abstract class Representation implements FormatWrapper { private final MultiSegmentBase segmentBase; /** - * @param periodStartMs The start time of the enclosing period in milliseconds. - * @param periodDurationMs The duration of the enclosing period in milliseconds, or -1 if the - * duration is unknown. * @param contentId Identifies the piece of content to which this representation belongs. * @param revisionId Identifies the revision of the content. * @param format The format of the representation. * @param segmentBase The segment base underlying the representation. */ - public MultiSegmentRepresentation(long periodStartMs, long periodDurationMs, String contentId, - long revisionId, Format format, MultiSegmentBase segmentBase) { - super(periodStartMs, periodDurationMs, contentId, revisionId, format, segmentBase); + public MultiSegmentRepresentation(String contentId, long revisionId, Format format, + MultiSegmentBase segmentBase) { + super(contentId, revisionId, format, segmentBase); this.segmentBase = segmentBase; } @@ -264,18 +237,18 @@ public abstract class Representation implements FormatWrapper { } @Override - public int getSegmentNum(long timeUs) { - return segmentBase.getSegmentNum(timeUs - periodStartMs * 1000); + public int getSegmentNum(long timeUs, long periodDurationUs) { + return segmentBase.getSegmentNum(timeUs, periodDurationUs); } @Override public long getTimeUs(int segmentIndex) { - return segmentBase.getSegmentTimeUs(segmentIndex) + periodStartMs * 1000; + return segmentBase.getSegmentTimeUs(segmentIndex); } @Override - public long getDurationUs(int segmentIndex) { - return segmentBase.getSegmentDurationUs(segmentIndex); + public long getDurationUs(int segmentIndex, long periodDurationUs) { + return segmentBase.getSegmentDurationUs(segmentIndex, periodDurationUs); } @Override @@ -284,8 +257,8 @@ public abstract class Representation implements FormatWrapper { } @Override - public int getLastSegmentNum() { - return segmentBase.getLastSegmentNum(); + public int getLastSegmentNum(long periodDurationUs) { + return segmentBase.getLastSegmentNum(periodDurationUs); } @Override diff --git a/library/src/main/java/com/google/android/exoplayer/dash/mpd/SegmentBase.java b/library/src/main/java/com/google/android/exoplayer/dash/mpd/SegmentBase.java index 6d55a01393..f770f304ba 100644 --- a/library/src/main/java/com/google/android/exoplayer/dash/mpd/SegmentBase.java +++ b/library/src/main/java/com/google/android/exoplayer/dash/mpd/SegmentBase.java @@ -112,7 +112,6 @@ public abstract class SegmentBase { */ public abstract static class MultiSegmentBase extends SegmentBase { - /* package */ final long periodDurationMs; /* package */ final int startNumber; /* package */ final long duration; /* package */ final List segmentTimeline; @@ -123,7 +122,6 @@ public abstract class SegmentBase { * @param timescale The timescale in units per second. * @param presentationTimeOffset The presentation time offset. The value in seconds is the * division of this value and {@code timescale}. - * @param periodDurationMs The duration of the enclosing period in milliseconds. * @param startNumber The sequence number of the first segment. * @param duration The duration of each segment in the case of fixed duration segments. The * value in seconds is the division of this value and {@code timescale}. If @@ -133,22 +131,20 @@ public abstract class SegmentBase { * parameter. */ public MultiSegmentBase(RangedUri initialization, long timescale, long presentationTimeOffset, - long periodDurationMs, int startNumber, long duration, - List segmentTimeline) { + int startNumber, long duration, List segmentTimeline) { super(initialization, timescale, presentationTimeOffset); - this.periodDurationMs = periodDurationMs; this.startNumber = startNumber; this.duration = duration; this.segmentTimeline = segmentTimeline; } /** - * @see DashSegmentIndex#getSegmentNum(long) + * @see DashSegmentIndex#getSegmentNum(long, long) */ - public int getSegmentNum(long timeUs) { + public int getSegmentNum(long timeUs, long periodDurationUs) { final int firstSegmentNum = getFirstSegmentNum(); int lowIndex = firstSegmentNum; - int highIndex = getLastSegmentNum(); + int highIndex = getLastSegmentNum(periodDurationUs); if (segmentTimeline == null) { // All segments are of equal duration (with the possible exception of the last one). long durationUs = (duration * C.MICROS_PER_SECOND) / timescale; @@ -175,15 +171,15 @@ public abstract class SegmentBase { } /** - * @see DashSegmentIndex#getDurationUs(int) + * @see DashSegmentIndex#getDurationUs(int, long) */ - public final long getSegmentDurationUs(int sequenceNumber) { + public final long getSegmentDurationUs(int sequenceNumber, long periodDurationUs) { if (segmentTimeline != null) { long duration = segmentTimeline.get(sequenceNumber - startNumber).duration; return (duration * C.MICROS_PER_SECOND) / timescale; } else { - return sequenceNumber == getLastSegmentNum() - ? ((periodDurationMs * 1000) - getSegmentTimeUs(sequenceNumber)) + return sequenceNumber == getLastSegmentNum(periodDurationUs) + ? (periodDurationUs - getSegmentTimeUs(sequenceNumber)) : ((duration * C.MICROS_PER_SECOND) / timescale); } } @@ -218,9 +214,9 @@ public abstract class SegmentBase { } /** - * @see DashSegmentIndex#getLastSegmentNum() + * @see DashSegmentIndex#getLastSegmentNum(long) */ - public abstract int getLastSegmentNum(); + public abstract int getLastSegmentNum(long periodDurationUs); /** * @see DashSegmentIndex#isExplicit() @@ -244,7 +240,6 @@ public abstract class SegmentBase { * @param timescale The timescale in units per second. * @param presentationTimeOffset The presentation time offset. The value in seconds is the * division of this value and {@code timescale}. - * @param periodDurationMs The duration of the enclosing period in milliseconds. * @param startNumber The sequence number of the first segment. * @param duration The duration of each segment in the case of fixed duration segments. The * value in seconds is the division of this value and {@code timescale}. If @@ -255,10 +250,10 @@ public abstract class SegmentBase { * @param mediaSegments A list of {@link RangedUri}s indicating the locations of the segments. */ public SegmentList(RangedUri initialization, long timescale, long presentationTimeOffset, - long periodDurationMs, int startNumber, long duration, - List segmentTimeline, List mediaSegments) { - super(initialization, timescale, presentationTimeOffset, periodDurationMs, startNumber, - duration, segmentTimeline); + int startNumber, long duration, List segmentTimeline, + List mediaSegments) { + super(initialization, timescale, presentationTimeOffset, startNumber, duration, + segmentTimeline); this.mediaSegments = mediaSegments; } @@ -268,7 +263,7 @@ public abstract class SegmentBase { } @Override - public int getLastSegmentNum() { + public int getLastSegmentNum(long periodDurationUs) { return startNumber + mediaSegments.size() - 1; } @@ -296,7 +291,6 @@ public abstract class SegmentBase { * @param timescale The timescale in units per second. * @param presentationTimeOffset The presentation time offset. The value in seconds is the * division of this value and {@code timescale}. - * @param periodDurationMs The duration of the enclosing period in milliseconds. * @param startNumber The sequence number of the first segment. * @param duration The duration of each segment in the case of fixed duration segments. The * value in seconds is the division of this value and {@code timescale}. If @@ -311,10 +305,9 @@ public abstract class SegmentBase { * @param baseUrl A url to use as the base for relative urls generated by the templates. */ public SegmentTemplate(RangedUri initialization, long timescale, long presentationTimeOffset, - long periodDurationMs, int startNumber, long duration, - List segmentTimeline, UrlTemplate initializationTemplate, - UrlTemplate mediaTemplate, String baseUrl) { - super(initialization, timescale, presentationTimeOffset, periodDurationMs, startNumber, + int startNumber, long duration, List segmentTimeline, + UrlTemplate initializationTemplate, UrlTemplate mediaTemplate, String baseUrl) { + super(initialization, timescale, presentationTimeOffset, startNumber, duration, segmentTimeline); this.initializationTemplate = initializationTemplate; this.mediaTemplate = mediaTemplate; @@ -346,14 +339,14 @@ public abstract class SegmentBase { } @Override - public int getLastSegmentNum() { + public int getLastSegmentNum(long periodDurationUs) { if (segmentTimeline != null) { return segmentTimeline.size() + startNumber - 1; - } else if (periodDurationMs == -1) { + } else if (periodDurationUs == C.UNKNOWN_TIME_US) { return DashSegmentIndex.INDEX_UNBOUNDED; } else { - long durationMs = (duration * 1000) / timescale; - return startNumber + (int) Util.ceilDivide(periodDurationMs, durationMs) - 1; + long durationUs = (duration * C.MICROS_PER_SECOND) / timescale; + return startNumber + (int) Util.ceilDivide(periodDurationUs, durationUs) - 1; } }