Refactor handling of period start times and durations.

- It's not possible to determine a period's duration just from the
corresponding Period element in a DASH manifest. It's necessary
to look at the start time of the next period (or the duration of the
manifest for the last period) to determine how long a period is. We
don't currently do this in the parser, and hence set duration incorrectly.
We also set period start times incorrectly because we don't set it to
equal to sum of the durations of prior periods in the case where it's
not explicitly defined.

- We're currently propagating these (incorrect) values all over the place
through data-structures that we build when parsing the Period element.

- This CL removes this redundancy, storing only the start time of each
period in Period elements, and not propagating it elsewhere. It's then
used when required in DashChunkSource.
This commit is contained in:
Oliver Woodman 2015-09-01 14:18:16 +01:00
parent cb85dc25aa
commit 2f4c96781d
14 changed files with 455 additions and 688 deletions

View File

@ -188,7 +188,7 @@ public class DashRendererBuilder implements RendererBuilder {
} }
private void buildRenderers() { private void buildRenderers() {
Period period = manifest.periods.get(0); Period period = manifest.getPeriod(0);
Handler mainHandler = player.getMainHandler(); Handler mainHandler = player.getMainHandler();
LoadControl loadControl = new DefaultLoadControl(new DefaultAllocator(BUFFER_SEGMENT_SIZE)); LoadControl loadControl = new DefaultLoadControl(new DefaultAllocator(BUFFER_SEGMENT_SIZE));
DefaultBandwidthMeter bandwidthMeter = new DefaultBandwidthMeter(mainHandler, player); DefaultBandwidthMeter bandwidthMeter = new DefaultBandwidthMeter(mainHandler, player);

View File

@ -24,7 +24,6 @@ import com.google.android.exoplayer.chunk.ChunkSampleSource;
import com.google.android.exoplayer.chunk.ChunkSource; import com.google.android.exoplayer.chunk.ChunkSource;
import com.google.android.exoplayer.chunk.FormatEvaluator; import com.google.android.exoplayer.chunk.FormatEvaluator;
import com.google.android.exoplayer.chunk.FormatEvaluator.AdaptiveEvaluator; 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.DashChunkSource;
import com.google.android.exoplayer.dash.mpd.AdaptationSet; import com.google.android.exoplayer.dash.mpd.AdaptationSet;
import com.google.android.exoplayer.dash.mpd.MediaPresentationDescription; import com.google.android.exoplayer.dash.mpd.MediaPresentationDescription;
@ -83,16 +82,16 @@ public class DashRendererBuilder implements ManifestCallback<MediaPresentationDe
DefaultBandwidthMeter bandwidthMeter = new DefaultBandwidthMeter(null, null); DefaultBandwidthMeter bandwidthMeter = new DefaultBandwidthMeter(null, null);
// Obtain Representations for playback. // Obtain Representations for playback.
ArrayList<Representation> audioRepresentationsList = new ArrayList<>(); Representation audioRepresentation = null;
ArrayList<Representation> videoRepresentationsList = new ArrayList<>(); ArrayList<Representation> videoRepresentationsList = new ArrayList<>();
Period period = manifest.periods.get(0); Period period = manifest.getPeriod(0);
for (int i = 0; i < period.adaptationSets.size(); i++) { for (int i = 0; i < period.adaptationSets.size(); i++) {
AdaptationSet adaptationSet = period.adaptationSets.get(i); AdaptationSet adaptationSet = period.adaptationSets.get(i);
int adaptationSetType = adaptationSet.type; int adaptationSetType = adaptationSet.type;
for (int j = 0; j < adaptationSet.representations.size(); j++) { for (int j = 0; j < adaptationSet.representations.size(); j++) {
Representation representation = adaptationSet.representations.get(j); Representation representation = adaptationSet.representations.get(j);
if (adaptationSetType == AdaptationSet.TYPE_AUDIO) { if (adaptationSetType == AdaptationSet.TYPE_AUDIO && audioRepresentation == null) {
audioRepresentationsList.add(representation); audioRepresentation = representation;
} else if (adaptationSetType == AdaptationSet.TYPE_VIDEO) { } else if (adaptationSetType == AdaptationSet.TYPE_VIDEO) {
videoRepresentationsList.add(representation); videoRepresentationsList.add(representation);
} }
@ -109,7 +108,8 @@ public class DashRendererBuilder implements ManifestCallback<MediaPresentationDe
String mimeType = videoRepresentations[0].format.mimeType; String mimeType = videoRepresentations[0].format.mimeType;
if (mimeType.equals(MimeTypes.VIDEO_WEBM)) { if (mimeType.equals(MimeTypes.VIDEO_WEBM)) {
videoChunkSource = new DashChunkSource(videoDataSource, videoChunkSource = new DashChunkSource(videoDataSource,
new AdaptiveEvaluator(bandwidthMeter), videoRepresentations); new AdaptiveEvaluator(bandwidthMeter), manifest.getPeriodDuration(0),
videoRepresentations);
} else { } else {
throw new IllegalStateException("Unexpected mime type: " + mimeType); throw new IllegalStateException("Unexpected mime type: " + mimeType);
} }
@ -120,21 +120,17 @@ public class DashRendererBuilder implements ManifestCallback<MediaPresentationDe
} }
// Build the audio renderer. // Build the audio renderer.
MultiTrackChunkSource audioChunkSource = null; TrackRenderer audioRenderer;
TrackRenderer audioRenderer = null; if (audioRepresentation == null) {
if (!audioRepresentationsList.isEmpty()) { audioRenderer = null;
} else {
DataSource audioDataSource = new DefaultUriDataSource(player, bandwidthMeter, userAgent); DataSource audioDataSource = new DefaultUriDataSource(player, bandwidthMeter, userAgent);
ChunkSource[] audioChunkSources = new ChunkSource[audioRepresentationsList.size()];
FormatEvaluator audioEvaluator = new FormatEvaluator.FixedEvaluator(); FormatEvaluator audioEvaluator = new FormatEvaluator.FixedEvaluator();
for (int i = 0; i < audioRepresentationsList.size(); i++) { DashChunkSource audioChunkSource = new DashChunkSource(audioDataSource, audioEvaluator,
Representation representation = audioRepresentationsList.get(i); manifest.getPeriodDuration(0), audioRepresentation);
audioChunkSources[i] = new DashChunkSource(audioDataSource,
audioEvaluator, representation);
}
audioChunkSource = new MultiTrackChunkSource(audioChunkSources);
SampleSource audioSampleSource = new ChunkSampleSource(audioChunkSource, loadControl, SampleSource audioSampleSource = new ChunkSampleSource(audioChunkSource, loadControl,
AUDIO_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE); AUDIO_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE);
if (manifestUrl.contains("opus")) { // TODO: Need a better logic here. if ("opus".equals(audioRepresentation.format.codecs)) {
audioRenderer = new LibopusAudioTrackRenderer(audioSampleSource); audioRenderer = new LibopusAudioTrackRenderer(audioSampleSource);
} else { } else {
audioRenderer = new MediaCodecAudioTrackRenderer(audioSampleSource); audioRenderer = new MediaCodecAudioTrackRenderer(audioSampleSource);

View File

@ -61,7 +61,6 @@ public class VideoPlayer extends Activity implements OnClickListener,
private static final int BUFFER_SEGMENT_SIZE = 64 * 1024; private static final int BUFFER_SEGMENT_SIZE = 64 * 1024;
private static final int BUFFER_SEGMENT_COUNT = 160; private static final int BUFFER_SEGMENT_COUNT = 160;
private boolean isDash; private boolean isDash;
private String manifestUrl; private String manifestUrl;
private boolean useOpenGL; private boolean useOpenGL;

View File

@ -19,7 +19,6 @@ import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
import com.google.android.exoplayer.TimeRange; import com.google.android.exoplayer.TimeRange;
import com.google.android.exoplayer.TrackRenderer;
import com.google.android.exoplayer.chunk.ChunkOperationHolder; import com.google.android.exoplayer.chunk.ChunkOperationHolder;
import com.google.android.exoplayer.chunk.Format; import com.google.android.exoplayer.chunk.Format;
import com.google.android.exoplayer.chunk.FormatEvaluator; import com.google.android.exoplayer.chunk.FormatEvaluator;
@ -41,11 +40,10 @@ import com.google.android.exoplayer.testutil.TestUtil;
import com.google.android.exoplayer.upstream.DataSource; import com.google.android.exoplayer.upstream.DataSource;
import com.google.android.exoplayer.util.FakeClock; import com.google.android.exoplayer.util.FakeClock;
import com.google.android.exoplayer.util.ManifestFetcher; import com.google.android.exoplayer.util.ManifestFetcher;
import com.google.android.exoplayer.util.Util;
import android.test.InstrumentationTestCase; import android.test.InstrumentationTestCase;
import org.mockito.Mock;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
@ -65,14 +63,11 @@ public class DashChunkSourceTest extends InstrumentationTestCase {
private static final long LIVE_TIMESHIFT_BUFFER_DEPTH_MS = LIVE_DURATION_MS; private static final long LIVE_TIMESHIFT_BUFFER_DEPTH_MS = LIVE_DURATION_MS;
private static final int MULTI_PERIOD_COUNT = 2; private static final int MULTI_PERIOD_COUNT = 2;
private static final long MULTI_PERIOD_VOD_DURATION_MS = VOD_DURATION_MS * MULTI_PERIOD_COUNT; private static final long MULTI_PERIOD_VOD_DURATION_MS = VOD_DURATION_MS * MULTI_PERIOD_COUNT;
private static final long MULTI_PERIOD_LIVE_DURATION_MS = LIVE_DURATION_MS * MULTI_PERIOD_COUNT; private static final long MULTI_PERIOD_LIVE_DURATION_MS = LIVE_DURATION_MS * MULTI_PERIOD_COUNT;
private static final long AVAILABILITY_START_TIME_MS = 60000; private static final long AVAILABILITY_START_TIME_MS = 60000;
private static final long AVAILABILITY_REALTIME_OFFSET_MS = 1000; private static final long ELAPSED_REALTIME_OFFSET_MS = 1000;
private static final long AVAILABILITY_CURRENT_TIME_MS =
AVAILABILITY_START_TIME_MS + LIVE_TIMESHIFT_BUFFER_DEPTH_MS - AVAILABILITY_REALTIME_OFFSET_MS;
private static final int TALL_HEIGHT = 200; private static final int TALL_HEIGHT = 200;
private static final int WIDE_WIDTH = 400; private static final int WIDE_WIDTH = 400;
@ -84,16 +79,13 @@ public class DashChunkSourceTest extends InstrumentationTestCase {
private static final Format WIDE_VIDEO = private static final Format WIDE_VIDEO =
new Format("3", "video/mp4", WIDE_WIDTH, 50, -1, -1, -1, 1000); new Format("3", "video/mp4", WIDE_WIDTH, 50, -1, -1, -1, 1000);
@Mock
private DataSource mockDataSource;
@Override @Override
public void setUp() throws Exception { public void setUp() throws Exception {
TestUtil.setUpMockito(this); TestUtil.setUpMockito(this);
} }
public void testGetAvailableRangeOnVod() { public void testGetAvailableRangeOnVod() {
DashChunkSource chunkSource = new DashChunkSource(generateVodMpd(), AdaptationSet.TYPE_VIDEO, DashChunkSource chunkSource = new DashChunkSource(buildVodMpd(), AdaptationSet.TYPE_VIDEO,
null, null, mock(FormatEvaluator.class)); null, null, mock(FormatEvaluator.class));
chunkSource.enable(0); chunkSource.enable(0);
TimeRange availableRange = chunkSource.getAvailableRange(); TimeRange availableRange = chunkSource.getAvailableRange();
@ -105,54 +97,31 @@ public class DashChunkSourceTest extends InstrumentationTestCase {
assertEquals(VOD_DURATION_MS, seekRangeValuesMs[1]); assertEquals(VOD_DURATION_MS, seekRangeValuesMs[1]);
} }
public void testGetAvailableRangeOnLiveWithTimelineNoEdgeLatency() { public void testGetAvailableRangeOnLiveWithTimeline() {
long liveEdgeLatency = 0; MediaPresentationDescription mpd = buildLiveMpdWithTimeline(LIVE_DURATION_MS, 0);
MediaPresentationDescription mpd = generateLiveMpdWithTimeline(0, 0, LIVE_DURATION_MS); DashChunkSource chunkSource = buildDashChunkSource(mpd);
DashChunkSource chunkSource = setupDashChunkSource(mpd, 0, liveEdgeLatency);
TimeRange availableRange = chunkSource.getAvailableRange(); TimeRange availableRange = chunkSource.getAvailableRange();
checkAvailableRange(availableRange, 0, LIVE_DURATION_MS * 1000);
}
public void testGetAvailableRangeOnLiveWithTimeline500msEdgeLatency() {
long liveEdgeLatency = 500;
MediaPresentationDescription mpd = generateLiveMpdWithTimeline(0, 0, LIVE_DURATION_MS);
DashChunkSource chunkSource = setupDashChunkSource(mpd, 0, liveEdgeLatency);
TimeRange availableRange = chunkSource.getAvailableRange();
checkAvailableRange(availableRange, 0, LIVE_DURATION_MS * 1000); checkAvailableRange(availableRange, 0, LIVE_DURATION_MS * 1000);
} }
public void testGetAvailableRangeOnMultiPeriodVod() { public void testGetAvailableRangeOnMultiPeriodVod() {
DashChunkSource chunkSource = new DashChunkSource(generateMultiPeriodVodMpd(), DashChunkSource chunkSource = new DashChunkSource(buildMultiPeriodVodMpd(),
AdaptationSet.TYPE_VIDEO, null, null, EVALUATOR); AdaptationSet.TYPE_VIDEO, null, null, EVALUATOR);
chunkSource.enable(0); chunkSource.enable(0);
TimeRange availableRange = chunkSource.getAvailableRange(); TimeRange availableRange = chunkSource.getAvailableRange();
checkAvailableRange(availableRange, 0, MULTI_PERIOD_VOD_DURATION_MS * 1000); checkAvailableRange(availableRange, 0, MULTI_PERIOD_VOD_DURATION_MS * 1000);
} }
public void testGetSeekRangeOnMultiPeriodLiveWithTimelineNoEdgeLatency() { public void testGetSeekRangeOnMultiPeriodLiveWithTimeline() {
long liveEdgeLatency = 0; MediaPresentationDescription mpd = buildMultiPeriodLiveMpdWithTimeline();
MediaPresentationDescription mpd = generateMultiPeriodLiveMpdWithTimeline(0); DashChunkSource chunkSource = buildDashChunkSource(mpd);
DashChunkSource chunkSource = setupDashChunkSource(mpd, 0, liveEdgeLatency);
TimeRange availableRange = chunkSource.getAvailableRange(); TimeRange availableRange = chunkSource.getAvailableRange();
checkAvailableRange(availableRange, 0, MULTI_PERIOD_LIVE_DURATION_MS * 1000);
}
public void testGetSeekRangeOnMultiPeriodLiveWithTimeline500msEdgeLatency() {
long liveEdgeLatency = 500;
MediaPresentationDescription mpd = generateMultiPeriodLiveMpdWithTimeline(0);
DashChunkSource chunkSource = setupDashChunkSource(mpd, 0, liveEdgeLatency);
TimeRange availableRange = chunkSource.getAvailableRange();
checkAvailableRange(availableRange, 0, MULTI_PERIOD_LIVE_DURATION_MS * 1000); checkAvailableRange(availableRange, 0, MULTI_PERIOD_LIVE_DURATION_MS * 1000);
} }
public void testSegmentIndexInitializationOnVod() { public void testSegmentIndexInitializationOnVod() {
DashChunkSource chunkSource = new DashChunkSource(generateVodMpd(), DashChunkSource chunkSource = new DashChunkSource(buildVodMpd(), AdaptationSet.TYPE_VIDEO,
AdaptationSet.TYPE_VIDEO, null, mockDataSource, EVALUATOR); null, mock(DataSource.class), EVALUATOR);
chunkSource.enable(0); chunkSource.enable(0);
List<MediaChunk> queue = new ArrayList<>(); List<MediaChunk> queue = new ArrayList<>();
@ -166,394 +135,258 @@ public class DashChunkSourceTest extends InstrumentationTestCase {
} }
public void testSegmentRequestSequenceOnMultiPeriodLiveWithTimeline() { public void testSegmentRequestSequenceOnMultiPeriodLiveWithTimeline() {
long liveEdgeLatency = 0; MediaPresentationDescription mpd = buildMultiPeriodLiveMpdWithTimeline();
MediaPresentationDescription mpd = generateMultiPeriodLiveMpdWithTimeline(0); DashChunkSource chunkSource = buildDashChunkSource(mpd);
DashChunkSource chunkSource = setupDashChunkSource(mpd, 0, liveEdgeLatency);
checkSegmentRequestSequenceOnMultiPeriodLive(chunkSource); checkSegmentRequestSequenceOnMultiPeriodLive(chunkSource);
} }
public void testSegmentRequestSequenceOnMultiPeriodLiveWithTemplate() { public void testSegmentRequestSequenceOnMultiPeriodLiveWithTemplate() {
long liveEdgeLatency = 0; MediaPresentationDescription mpd = buildMultiPeriodLiveMpdWithTemplate();
MediaPresentationDescription mpd = generateMultiPeriodLiveMpdWithTemplate(0); DashChunkSource chunkSource = buildDashChunkSource(mpd);
DashChunkSource chunkSource = setupDashChunkSource(mpd, 0, liveEdgeLatency,
AVAILABILITY_CURRENT_TIME_MS + LIVE_DURATION_MS);
checkSegmentRequestSequenceOnMultiPeriodLive(chunkSource); checkSegmentRequestSequenceOnMultiPeriodLive(chunkSource);
} }
public void testLiveEdgeLatency() {
public void testLiveEdgeNoLatency() {
long startTimeMs = 0;
long liveEdgeLatencyMs = 0;
long seekPositionMs = startTimeMs + LIVE_DURATION_MS - liveEdgeLatencyMs;
long availableRangeStartMs = 0; long availableRangeStartMs = 0;
long availableRangeEndMs = LIVE_DURATION_MS; long availableRangeEndMs = LIVE_DURATION_MS;
long seekPositionMs = LIVE_DURATION_MS;
long chunkStartTimeMs = 4000; long chunkStartTimeMs = 4000;
long chunkEndTimeMs = 5000; 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); availableRangeStartMs, availableRangeEndMs, chunkStartTimeMs, chunkEndTimeMs);
} }
public void testLiveEdgeAlmostNoLatency() { // Private methods.
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;
checkLiveTimelineConsistency(startTimeMs, liveEdgeLatencyMs, seekPositionMs, private static Representation buildVodRepresentation(Format format) {
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) {
RangedUri rangedUri = new RangedUri("https://example.com/1.mp4", null, 0, 100); RangedUri rangedUri = new RangedUri("https://example.com/1.mp4", null, 0, 100);
SingleSegmentBase segmentBase = new SingleSegmentBase(rangedUri, 1, 0, SingleSegmentBase segmentBase = new SingleSegmentBase(rangedUri, 1, 0,
"https://example.com/1.mp4", 0, -1); "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, private static Representation buildSegmentTimelineRepresentation(long timelineDurationMs,
long periodStartMs, long duration) { long timelineStartTimeMs) {
List<SegmentTimelineElement> segmentTimeline = new ArrayList<>(); List<SegmentTimelineElement> segmentTimeline = new ArrayList<>();
List<RangedUri> mediaSegments = new ArrayList<>(); List<RangedUri> mediaSegments = new ArrayList<>();
long segmentStartTimeMs = segmentStartMs; long segmentStartTimeMs = timelineStartTimeMs;
long byteStart = 0; 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)); segmentTimeline.add(new SegmentTimelineElement(segmentStartTimeMs, LIVE_SEGMENT_DURATION_MS));
mediaSegments.add(new RangedUri("", "", byteStart, 500L)); mediaSegments.add(new RangedUri("", "", byteStart, 500L));
segmentStartTimeMs += LIVE_SEGMENT_DURATION_MS; segmentStartTimeMs += LIVE_SEGMENT_DURATION_MS;
byteStart += 500; byteStart += 500;
} }
// The final segment duration is calculated so that the total duration is timelineDurationMs.
int startNumber = (int) ((periodStartMs + segmentStartMs) / LIVE_SEGMENT_DURATION_MS); long finalSegmentDurationMs = (timelineStartTimeMs + timelineDurationMs) - segmentStartTimeMs;
MultiSegmentBase segmentBase = new SegmentList(null, 1000, 0, segmentTimeline.add(new SegmentTimelineElement(segmentStartTimeMs, finalSegmentDurationMs));
TrackRenderer.UNKNOWN_TIME_US, startNumber, TrackRenderer.UNKNOWN_TIME_US, segmentTimeline, mediaSegments.add(new RangedUri("", "", byteStart, 500L));
segmentStartTimeMs += finalSegmentDurationMs;
byteStart += 500;
// Construct the list.
MultiSegmentBase segmentBase = new SegmentList(null, 1000, 0, 0, 0, segmentTimeline,
mediaSegments); mediaSegments);
return Representation.newInstance(periodStartMs, TrackRenderer.UNKNOWN_TIME_US, null, 0, return Representation.newInstance(null, 0, REGULAR_VIDEO, segmentBase);
REGULAR_VIDEO, segmentBase);
} }
private static Representation generateSegmentTemplateRepresentation(long periodStartMs, private static Representation buildSegmentTemplateRepresentation() {
long periodDurationMs) {
UrlTemplate initializationTemplate = null; UrlTemplate initializationTemplate = null;
UrlTemplate mediaTemplate = UrlTemplate.compile("$RepresentationID$/$Number$"); UrlTemplate mediaTemplate = UrlTemplate.compile("$RepresentationID$/$Number$");
int startNumber = (int) (periodStartMs / LIVE_SEGMENT_DURATION_MS); MultiSegmentBase segmentBase = new SegmentTemplate(null, 1000, 0, 0, LIVE_SEGMENT_DURATION_MS,
MultiSegmentBase segmentBase = new SegmentTemplate(null, 1000, 0, null, initializationTemplate, mediaTemplate, "http://www.youtube.com");
periodDurationMs, startNumber, LIVE_SEGMENT_DURATION_MS, null, return Representation.newInstance(null, 0, REGULAR_VIDEO, segmentBase);
initializationTemplate, mediaTemplate, "http://www.youtube.com");
return Representation.newInstance(periodStartMs, periodDurationMs, null, 0, REGULAR_VIDEO,
segmentBase);
} }
private static MediaPresentationDescription generateMpd(boolean live, private static MediaPresentationDescription buildMpd(long durationMs,
List<Representation> representations, boolean limitTimeshiftBuffer) { List<Representation> representations, boolean live, boolean limitTimeshiftBuffer) {
Representation firstRepresentation = representations.get(0);
AdaptationSet adaptationSet = new AdaptationSet(0, AdaptationSet.TYPE_UNKNOWN, representations); AdaptationSet adaptationSet = new AdaptationSet(0, AdaptationSet.TYPE_UNKNOWN, representations);
Period period = new Period(null, firstRepresentation.periodStartMs, Period period = new Period(null, 0, Collections.singletonList(adaptationSet));
firstRepresentation.periodDurationMs, Collections.singletonList(adaptationSet)); return new MediaPresentationDescription(AVAILABILITY_START_TIME_MS, durationMs, -1, live, -1,
long duration = (live) ? TrackRenderer.UNKNOWN_TIME_US
: firstRepresentation.periodDurationMs - firstRepresentation.periodStartMs;
return new MediaPresentationDescription(AVAILABILITY_START_TIME_MS, duration, -1, live, -1,
(limitTimeshiftBuffer) ? LIVE_TIMESHIFT_BUFFER_DEPTH_MS : -1, null, null, (limitTimeshiftBuffer) ? LIVE_TIMESHIFT_BUFFER_DEPTH_MS : -1, null, null,
Collections.singletonList(period)); Collections.singletonList(period));
} }
private static MediaPresentationDescription generateMultiPeriodMpd(boolean live, private static MediaPresentationDescription buildMultiPeriodMpd(long durationMs,
List<Period> periods, boolean limitTimeshiftBuffer) { List<Period> periods, boolean live, boolean limitTimeshiftBuffer) {
Period firstPeriod = periods.get(0); return new MediaPresentationDescription(AVAILABILITY_START_TIME_MS, durationMs, -1, live, -1,
Period lastPeriod = periods.get(periods.size() - 1);
long duration = (live) ? TrackRenderer.UNKNOWN_TIME_US
: (lastPeriod.startMs + lastPeriod.durationMs - firstPeriod.startMs);
return new MediaPresentationDescription(AVAILABILITY_START_TIME_MS, duration, -1, live, -1,
(limitTimeshiftBuffer) ? LIVE_TIMESHIFT_BUFFER_DEPTH_MS : -1, (limitTimeshiftBuffer) ? LIVE_TIMESHIFT_BUFFER_DEPTH_MS : -1,
null, null, periods); null, null, periods);
} }
private static MediaPresentationDescription generateVodMpd() { private static MediaPresentationDescription buildVodMpd() {
List<Representation> representations = new ArrayList<>(); List<Representation> representations = new ArrayList<>();
representations.add(buildVodRepresentation(TALL_VIDEO));
representations.add(generateVodRepresentation(0, VOD_DURATION_MS, TALL_VIDEO)); representations.add(buildVodRepresentation(WIDE_VIDEO));
representations.add(generateVodRepresentation(0, VOD_DURATION_MS, WIDE_VIDEO)); return buildMpd(VOD_DURATION_MS, representations, false, false);
return generateMpd(false, representations, false);
} }
private MediaPresentationDescription generateMultiPeriodVodMpd() { private static MediaPresentationDescription buildMultiPeriodVodMpd() {
List<Period> periods = new ArrayList<>(); List<Period> periods = new ArrayList<>();
long startTimeMs = 0; long timeMs = 0;
long periodDurationMs = VOD_DURATION_MS;
long duration = VOD_DURATION_MS;
for (int i = 0; i < 2; i++) { for (int i = 0; i < 2; i++) {
Representation representation = generateVodRepresentation(startTimeMs, duration, Representation representation = buildVodRepresentation(REGULAR_VIDEO);
REGULAR_VIDEO);
AdaptationSet adaptationSet = new AdaptationSet(0, AdaptationSet.TYPE_UNKNOWN, AdaptationSet adaptationSet = new AdaptationSet(0, AdaptationSet.TYPE_UNKNOWN,
Collections.singletonList(representation)); Collections.singletonList(representation));
Period period = new Period(null, startTimeMs, duration, Period period = new Period(null, timeMs, Collections.singletonList(adaptationSet));
Collections.singletonList(adaptationSet));
periods.add(period); periods.add(period);
startTimeMs += duration; timeMs += periodDurationMs;
}
return buildMultiPeriodMpd(timeMs, periods, false, false);
} }
return generateMultiPeriodMpd(false, periods, 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 generateLiveMpdWithTimeline(long segmentStartMs, private static MediaPresentationDescription buildLiveMpdWithTemplate(long durationMs,
long periodStartMs, long durationMs) { boolean limitTimeshiftBuffer) {
return generateMpd(true, Collections.singletonList(generateSegmentTimelineRepresentation( Representation representation = buildSegmentTemplateRepresentation();
segmentStartMs, periodStartMs, durationMs)), false); return buildMpd(durationMs, Collections.singletonList(representation), true,
limitTimeshiftBuffer);
} }
private static MediaPresentationDescription generateLiveMpdWithTemplate(long periodStartMs, private static MediaPresentationDescription buildMultiPeriodLiveMpdWithTimeline() {
long periodDurationMs, boolean limitTimeshiftBuffer) {
return generateMpd(true, Collections.singletonList(generateSegmentTemplateRepresentation(
periodStartMs, periodDurationMs)), limitTimeshiftBuffer);
}
private static MediaPresentationDescription generateMultiPeriodLiveMpdWithTimeline(
long startTimeMs) {
List<Period> periods = new ArrayList<>(); List<Period> periods = new ArrayList<>();
long periodStartTimeMs = 0;
long periodDurationMs = LIVE_DURATION_MS;
for (int i = 0; i < MULTI_PERIOD_COUNT; i++) { for (int i = 0; i < MULTI_PERIOD_COUNT; i++) {
Representation representation = generateSegmentTimelineRepresentation(0, startTimeMs, Representation representation = buildSegmentTimelineRepresentation(LIVE_DURATION_MS, 0);
LIVE_DURATION_MS);
AdaptationSet adaptationSet = new AdaptationSet(0, AdaptationSet.TYPE_UNKNOWN, AdaptationSet adaptationSet = new AdaptationSet(0, AdaptationSet.TYPE_UNKNOWN,
Collections.singletonList(representation)); Collections.singletonList(representation));
long duration = (i < MULTI_PERIOD_COUNT - 1) ? MULTI_PERIOD_COUNT Period period = new Period(null, periodStartTimeMs, Collections.singletonList(adaptationSet));
: TrackRenderer.END_OF_TRACK_US;
Period period = new Period(null, startTimeMs, duration,
Collections.singletonList(adaptationSet));
periods.add(period); periods.add(period);
startTimeMs += LIVE_DURATION_MS; periodStartTimeMs += periodDurationMs;
}
return buildMultiPeriodMpd(periodDurationMs, periods, true, false);
} }
return generateMultiPeriodMpd(true, periods, false); private static MediaPresentationDescription buildMultiPeriodLiveMpdWithTemplate() {
}
private static MediaPresentationDescription generateMultiPeriodLiveMpdWithTemplate(
long periodStartTimeMs) {
List<Period> periods = new ArrayList<>(); List<Period> periods = new ArrayList<>();
long periodStartTimeMs = 0;
Representation representation1 = generateSegmentTemplateRepresentation(periodStartTimeMs, long periodDurationMs = LIVE_DURATION_MS;
LIVE_DURATION_MS); for (int i = 0; i < MULTI_PERIOD_COUNT; i++) {
AdaptationSet adaptationSet1 = new AdaptationSet(0, AdaptationSet.TYPE_UNKNOWN, Representation representation = buildSegmentTemplateRepresentation();
Collections.singletonList(representation1)); AdaptationSet adaptationSet = new AdaptationSet(0, AdaptationSet.TYPE_UNKNOWN,
Period period1 = new Period(null, periodStartTimeMs, LIVE_DURATION_MS, Collections.singletonList(representation));
Collections.singletonList(adaptationSet1)); Period period = new Period(null, periodStartTimeMs, Collections.singletonList(adaptationSet));
periods.add(period1); periods.add(period);
periodStartTimeMs += periodDurationMs;
periodStartTimeMs += LIVE_DURATION_MS; }
return buildMultiPeriodMpd(MULTI_PERIOD_LIVE_DURATION_MS, periods, true, false);
Representation representation2 = generateSegmentTemplateRepresentation(periodStartTimeMs,
TrackRenderer.UNKNOWN_TIME_US);
AdaptationSet adaptationSet2 = new AdaptationSet(0, AdaptationSet.TYPE_UNKNOWN,
Collections.singletonList(representation2));
Period period2 = new Period(null, periodStartTimeMs, TrackRenderer.UNKNOWN_TIME_US,
Collections.singletonList(adaptationSet2));
periods.add(period2);
return generateMultiPeriodMpd(true, periods, false);
} }
private DashChunkSource setupDashChunkSource(MediaPresentationDescription mpd, long periodStartMs, private static DashChunkSource buildDashChunkSource(MediaPresentationDescription mpd) {
long liveEdgeLatencyMs) { return buildDashChunkSource(mpd, false, 0);
return setupDashChunkSource(mpd, periodStartMs, liveEdgeLatencyMs,
AVAILABILITY_CURRENT_TIME_MS + periodStartMs);
} }
@SuppressWarnings("unused") private static DashChunkSource buildDashChunkSource(MediaPresentationDescription mpd,
private DashChunkSource setupDashChunkSource(MediaPresentationDescription mpd, long periodStartMs, boolean startAtLiveEdge, long liveEdgeLatencyMs) {
long liveEdgeLatencyMs, long nowUs) {
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
ManifestFetcher<MediaPresentationDescription> manifestFetcher = mock(ManifestFetcher.class); ManifestFetcher<MediaPresentationDescription> manifestFetcher = mock(ManifestFetcher.class);
when(manifestFetcher.getManifest()).thenReturn(mpd); when(manifestFetcher.getManifest()).thenReturn(mpd);
DashChunkSource chunkSource = new DashChunkSource(manifestFetcher, mpd, DashChunkSource chunkSource = new DashChunkSource(manifestFetcher, mpd,
AdaptationSet.TYPE_VIDEO, null, mockDataSource, EVALUATOR, AdaptationSet.TYPE_VIDEO, null, mock(DataSource.class), EVALUATOR,
new FakeClock(nowUs), liveEdgeLatencyMs * 1000, AVAILABILITY_REALTIME_OFFSET_MS * 1000, new FakeClock(mpd.availabilityStartTime + mpd.duration - ELAPSED_REALTIME_OFFSET_MS),
false, null, null); liveEdgeLatencyMs * 1000, ELAPSED_REALTIME_OFFSET_MS * 1000, startAtLiveEdge, null, null);
chunkSource.enable(0); chunkSource.enable(0);
return chunkSource; 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); long[] seekRangeValuesUs = seekRange.getCurrentBoundsUs(null);
assertEquals(startTimeUs, seekRangeValuesUs[0]); assertEquals(startTimeUs, seekRangeValuesUs[0]);
assertEquals(endTimeUs, seekRangeValuesUs[1]); assertEquals(endTimeUs, seekRangeValuesUs[1]);
} }
private void checkLiveEdgeLatency(DashChunkSource chunkSource, List<MediaChunk> queue, private 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<MediaChunk> queue = new ArrayList<>();
ChunkOperationHolder out = new ChunkOperationHolder();
checkLiveEdgeConsistency(chunkSource, queue, out, seekPositionMs, availableRangeStartMs,
availableRangeEndMs, chunkStartTimeMs, chunkEndTimeMs);
}
private static void checkLiveEdgeConsistency(DashChunkSource chunkSource, List<MediaChunk> queue,
ChunkOperationHolder out, long seekPositionMs, long availableRangeStartMs, ChunkOperationHolder out, long seekPositionMs, long availableRangeStartMs,
long availableRangeEndMs, long chunkStartTimeMs, long chunkEndTimeMs) { long availableRangeEndMs, long chunkStartTimeMs, long chunkEndTimeMs) {
chunkSource.getChunkOperation(queue, seekPositionMs * 1000, 0, out); chunkSource.getChunkOperation(queue, seekPositionMs * 1000, 0, out);
TimeRange availableRange = chunkSource.getAvailableRange(); TimeRange availableRange = chunkSource.getAvailableRange();
checkAvailableRange(availableRange, availableRangeStartMs * 1000, availableRangeEndMs * 1000); checkAvailableRange(availableRange, availableRangeStartMs * 1000, availableRangeEndMs * 1000);
if (chunkStartTimeMs < availableRangeEndMs) { if (chunkStartTimeMs < availableRangeEndMs) {
assertNotNull(out.chunk); assertNotNull(out.chunk);
@ -564,62 +397,7 @@ public class DashChunkSourceTest extends InstrumentationTestCase {
} }
} }
private void checkLiveEdgeLatency(MediaPresentationDescription mpd, long periodStartMs, private static void checkSegmentRequestSequenceOnMultiPeriodLive(DashChunkSource chunkSource) {
long liveEdgeLatencyMs, long seekPositionMs, long availableRangeStartMs,
long availableRangeEndMs, long chunkStartTimeMs, long chunkEndTimeMs) {
DashChunkSource chunkSource = setupDashChunkSource(mpd, periodStartMs, liveEdgeLatencyMs);
List<MediaChunk> queue = new ArrayList<>();
ChunkOperationHolder out = new ChunkOperationHolder();
checkLiveEdgeLatency(chunkSource, queue, out, seekPositionMs, 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) {
List<MediaChunk> queue = new ArrayList<>(); List<MediaChunk> queue = new ArrayList<>();
ChunkOperationHolder out = new ChunkOperationHolder(); ChunkOperationHolder out = new ChunkOperationHolder();
@ -630,7 +408,7 @@ public class DashChunkSourceTest extends InstrumentationTestCase {
long chunkEndTimeMs = 1000; long chunkEndTimeMs = 1000;
// request first chunk // request first chunk
checkLiveEdgeLatency(chunkSource, queue, out, seekPositionMs, checkLiveEdgeConsistency(chunkSource, queue, out, seekPositionMs,
availableRangeStartMs, availableRangeEndMs, chunkStartTimeMs, chunkEndTimeMs); availableRangeStartMs, availableRangeEndMs, chunkStartTimeMs, chunkEndTimeMs);
queue.add((MediaChunk) out.chunk); queue.add((MediaChunk) out.chunk);
@ -638,7 +416,7 @@ public class DashChunkSourceTest extends InstrumentationTestCase {
chunkStartTimeMs += 1000; chunkStartTimeMs += 1000;
chunkEndTimeMs += 1000; chunkEndTimeMs += 1000;
out.chunk = null; out.chunk = null;
checkLiveEdgeLatency(chunkSource, queue, out, seekPositionMs, checkLiveEdgeConsistency(chunkSource, queue, out, seekPositionMs,
availableRangeStartMs, availableRangeEndMs, chunkStartTimeMs, chunkEndTimeMs); availableRangeStartMs, availableRangeEndMs, chunkStartTimeMs, chunkEndTimeMs);
queue.add((MediaChunk) out.chunk); queue.add((MediaChunk) out.chunk);
@ -646,7 +424,7 @@ public class DashChunkSourceTest extends InstrumentationTestCase {
chunkStartTimeMs += 1000; chunkStartTimeMs += 1000;
chunkEndTimeMs += 1000; chunkEndTimeMs += 1000;
out.chunk = null; out.chunk = null;
checkLiveEdgeLatency(chunkSource, queue, out, seekPositionMs, checkLiveEdgeConsistency(chunkSource, queue, out, seekPositionMs,
availableRangeStartMs, availableRangeEndMs, chunkStartTimeMs, chunkEndTimeMs); availableRangeStartMs, availableRangeEndMs, chunkStartTimeMs, chunkEndTimeMs);
queue.add((MediaChunk) out.chunk); queue.add((MediaChunk) out.chunk);
@ -654,7 +432,7 @@ public class DashChunkSourceTest extends InstrumentationTestCase {
chunkStartTimeMs += 1000; chunkStartTimeMs += 1000;
chunkEndTimeMs += 1000; chunkEndTimeMs += 1000;
out.chunk = null; out.chunk = null;
checkLiveEdgeLatency(chunkSource, queue, out, seekPositionMs, checkLiveEdgeConsistency(chunkSource, queue, out, seekPositionMs,
availableRangeStartMs, availableRangeEndMs, chunkStartTimeMs, chunkEndTimeMs); availableRangeStartMs, availableRangeEndMs, chunkStartTimeMs, chunkEndTimeMs);
queue.add((MediaChunk) out.chunk); queue.add((MediaChunk) out.chunk);
@ -662,7 +440,7 @@ public class DashChunkSourceTest extends InstrumentationTestCase {
chunkStartTimeMs += 1000; chunkStartTimeMs += 1000;
chunkEndTimeMs += 1000; chunkEndTimeMs += 1000;
out.chunk = null; out.chunk = null;
checkLiveEdgeLatency(chunkSource, queue, out, seekPositionMs, checkLiveEdgeConsistency(chunkSource, queue, out, seekPositionMs,
availableRangeStartMs, availableRangeEndMs, chunkStartTimeMs, chunkEndTimeMs); availableRangeStartMs, availableRangeEndMs, chunkStartTimeMs, chunkEndTimeMs);
queue.add((MediaChunk) out.chunk); queue.add((MediaChunk) out.chunk);
@ -670,7 +448,7 @@ public class DashChunkSourceTest extends InstrumentationTestCase {
chunkStartTimeMs += 1000; chunkStartTimeMs += 1000;
chunkEndTimeMs += 1000; chunkEndTimeMs += 1000;
out.chunk = null; out.chunk = null;
checkLiveEdgeLatency(chunkSource, queue, out, seekPositionMs, checkLiveEdgeConsistency(chunkSource, queue, out, seekPositionMs,
availableRangeStartMs, availableRangeEndMs, chunkStartTimeMs, chunkEndTimeMs); availableRangeStartMs, availableRangeEndMs, chunkStartTimeMs, chunkEndTimeMs);
queue.add((MediaChunk) out.chunk); queue.add((MediaChunk) out.chunk);
@ -678,7 +456,7 @@ public class DashChunkSourceTest extends InstrumentationTestCase {
chunkStartTimeMs += 1000; chunkStartTimeMs += 1000;
chunkEndTimeMs += 1000; chunkEndTimeMs += 1000;
out.chunk = null; out.chunk = null;
checkLiveEdgeLatency(chunkSource, queue, out, seekPositionMs, checkLiveEdgeConsistency(chunkSource, queue, out, seekPositionMs,
availableRangeStartMs, availableRangeEndMs, chunkStartTimeMs, chunkEndTimeMs); availableRangeStartMs, availableRangeEndMs, chunkStartTimeMs, chunkEndTimeMs);
queue.add((MediaChunk) out.chunk); queue.add((MediaChunk) out.chunk);
@ -686,7 +464,7 @@ public class DashChunkSourceTest extends InstrumentationTestCase {
chunkStartTimeMs += 1000; chunkStartTimeMs += 1000;
chunkEndTimeMs += 1000; chunkEndTimeMs += 1000;
out.chunk = null; out.chunk = null;
checkLiveEdgeLatency(chunkSource, queue, out, seekPositionMs, checkLiveEdgeConsistency(chunkSource, queue, out, seekPositionMs,
availableRangeStartMs, availableRangeEndMs, chunkStartTimeMs, chunkEndTimeMs); availableRangeStartMs, availableRangeEndMs, chunkStartTimeMs, chunkEndTimeMs);
queue.add((MediaChunk) out.chunk); queue.add((MediaChunk) out.chunk);
@ -694,7 +472,7 @@ public class DashChunkSourceTest extends InstrumentationTestCase {
chunkStartTimeMs += 1000; chunkStartTimeMs += 1000;
chunkEndTimeMs += 1000; chunkEndTimeMs += 1000;
out.chunk = null; out.chunk = null;
checkLiveEdgeLatency(chunkSource, queue, out, seekPositionMs, checkLiveEdgeConsistency(chunkSource, queue, out, seekPositionMs,
availableRangeStartMs, availableRangeEndMs, chunkStartTimeMs, chunkEndTimeMs); availableRangeStartMs, availableRangeEndMs, chunkStartTimeMs, chunkEndTimeMs);
queue.add((MediaChunk) out.chunk); queue.add((MediaChunk) out.chunk);
@ -702,7 +480,7 @@ public class DashChunkSourceTest extends InstrumentationTestCase {
chunkStartTimeMs += 1000; chunkStartTimeMs += 1000;
chunkEndTimeMs += 1000; chunkEndTimeMs += 1000;
out.chunk = null; out.chunk = null;
checkLiveEdgeLatency(chunkSource, queue, out, seekPositionMs, checkLiveEdgeConsistency(chunkSource, queue, out, seekPositionMs,
availableRangeStartMs, availableRangeEndMs, chunkStartTimeMs, chunkEndTimeMs); availableRangeStartMs, availableRangeEndMs, chunkStartTimeMs, chunkEndTimeMs);
queue.add((MediaChunk) out.chunk); queue.add((MediaChunk) out.chunk);

View File

@ -30,12 +30,11 @@ public class RepresentationTest extends TestCase {
String uri = "http://www.google.com"; String uri = "http://www.google.com";
SegmentBase base = new SingleSegmentBase(new RangedUri(uri, null, 0, 1), 1, 0, uri, 1, 1); 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); Format format = new Format("0", MimeTypes.VIDEO_MP4, 1920, 1080, -1, 0, 0, 2500000);
Representation representation = Representation.newInstance(-1, -1, "test_stream_1", 3, Representation representation = Representation.newInstance("test_stream_1", 3, format, base);
format, base);
assertEquals("test_stream_1.0.3", representation.getCacheKey()); assertEquals("test_stream_1.0.3", representation.getCacheKey());
format = new Format("150", MimeTypes.VIDEO_MP4, 1920, 1080, -1, 0, 0, 2500000); 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()); assertEquals("test_stream_1.150.-1", representation.getCacheKey());
} }

View File

@ -16,11 +16,11 @@
package com.google.android.exoplayer.dash; package com.google.android.exoplayer.dash;
import com.google.android.exoplayer.BehindLiveWindowException; import com.google.android.exoplayer.BehindLiveWindowException;
import com.google.android.exoplayer.C;
import com.google.android.exoplayer.MediaFormat; import com.google.android.exoplayer.MediaFormat;
import com.google.android.exoplayer.TimeRange; import com.google.android.exoplayer.TimeRange;
import com.google.android.exoplayer.TimeRange.DynamicTimeRange; import com.google.android.exoplayer.TimeRange.DynamicTimeRange;
import com.google.android.exoplayer.TimeRange.StaticTimeRange; 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.Chunk;
import com.google.android.exoplayer.chunk.ChunkExtractorWrapper; import com.google.android.exoplayer.chunk.ChunkExtractorWrapper;
import com.google.android.exoplayer.chunk.ChunkOperationHolder; 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 dataSource A {@link DataSource} suitable for loading the media data.
* @param formatEvaluator Selects from the available formats. * @param formatEvaluator Selects from the available formats.
* @param durationMs The duration of the content.
* @param representations The representations to be considered by the source. * @param representations The representations to be considered by the source.
*/ */
public DashChunkSource(DataSource dataSource, FormatEvaluator formatEvaluator, public DashChunkSource(DataSource dataSource, FormatEvaluator formatEvaluator,
Representation... representations) { long durationMs, Representation... representations) {
this(buildManifest(Arrays.asList(representations)), 0, null, dataSource, formatEvaluator); 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 dataSource A {@link DataSource} suitable for loading the media data.
* @param formatEvaluator Selects from the available formats. * @param formatEvaluator Selects from the available formats.
* @param durationMs The duration of the content.
* @param representations The representations to be considered by the source. * @param representations The representations to be considered by the source.
*/ */
public DashChunkSource(DataSource dataSource, FormatEvaluator formatEvaluator, public DashChunkSource(DataSource dataSource, FormatEvaluator formatEvaluator,
List<Representation> representations) { long durationMs, List<Representation> representations) {
this(buildManifest(representations), 0, null, dataSource, formatEvaluator); this(buildManifest(durationMs, representations), 0, null, dataSource, formatEvaluator);
} }
/** /**
@ -277,9 +280,9 @@ public class DashChunkSource implements ChunkSource {
String mimeType = ""; String mimeType = "";
for (int i = 0; i < periodHolders.size(); i++) { for (int i = 0; i < periodHolders.size(); i++) {
PeriodHolder periodHolder = periodHolders.valueAt(i); PeriodHolder periodHolder = periodHolders.valueAt(i);
if (totalDurationUs != TrackRenderer.UNKNOWN_TIME_US) { if (totalDurationUs != C.UNKNOWN_TIME_US) {
if (periodHolder.durationUs == TrackRenderer.UNKNOWN_TIME_US) { if (periodHolder.durationUs == C.UNKNOWN_TIME_US) {
totalDurationUs = TrackRenderer.UNKNOWN_TIME_US; totalDurationUs = C.UNKNOWN_TIME_US;
} else { } else {
totalDurationUs += periodHolder.durationUs; 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. // In all cases where we return before instantiating a new chunk, we want out.chunk to be null.
out.chunk = null; out.chunk = null;
long segmentStartTimeUs; boolean startingNewPeriod;
int segmentNum = -1;
boolean startingNewPeriod = false;
PeriodHolder periodHolder; PeriodHolder periodHolder;
availableRange.getCurrentBoundsUs(availableRangeValues); availableRange.getCurrentBoundsUs(availableRangeValues);
@ -415,7 +416,6 @@ public class DashChunkSource implements ChunkSource {
} }
periodHolder = findPeriodHolder(seekPositionUs); periodHolder = findPeriodHolder(seekPositionUs);
segmentStartTimeUs = seekPositionUs;
startingNewPeriod = true; startingNewPeriod = true;
} else { } else {
if (startAtLiveEdge) { if (startAtLiveEdge) {
@ -430,15 +430,13 @@ public class DashChunkSource implements ChunkSource {
return; return;
} }
segmentNum = previous.chunkIndex + 1;
segmentStartTimeUs = previous.endTimeUs;
if (currentManifest.dynamic) { 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. // This is before the first chunk in the current manifest.
fatalError = new BehindLiveWindowException(); fatalError = new BehindLiveWindowException();
return; 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 // 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 // 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. // while before attempting to load the chunk.
@ -446,23 +444,25 @@ public class DashChunkSource implements ChunkSource {
} }
} }
startingNewPeriod = false;
periodHolder = periodHolders.get(previous.parentId); periodHolder = periodHolders.get(previous.parentId);
if (periodHolder == null) { 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 // 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 // (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); periodHolder = periodHolders.valueAt(0);
startingNewPeriod = true; startingNewPeriod = true;
} else if (!periodHolder.isIndexUnbounded() } else if (!periodHolder.isIndexUnbounded()) {
&& segmentStartTimeUs >= periodHolder.getAvailableEndTimeUs()) { RepresentationHolder representationHolder =
// we reached the end of a period, start the next one (note that we can't actually periodHolder.representationHolders.get(previous.format.id);
// update the segmentNum yet because the new period might have a different if (representationHolder.isLastSegment(previous.chunkIndex)) {
// sequence and it's segmentIndex might not have been loaded yet) // We reached the end of a period. Start the next one.
periodHolder = periodHolders.get(previous.parentId + 1); periodHolder = periodHolders.get(previous.parentId + 1);
startingNewPeriod = true; startingNewPeriod = true;
} }
} }
}
RepresentationHolder representationHolder = RepresentationHolder representationHolder =
periodHolder.representationHolders.get(selectedFormat.id); periodHolder.representationHolders.get(selectedFormat.id);
@ -483,24 +483,16 @@ public class DashChunkSource implements ChunkSource {
if (pendingInitializationUri != null || pendingIndexUri != null) { if (pendingInitializationUri != null || pendingIndexUri != null) {
// We have initialization and/or index requests to make. // We have initialization and/or index requests to make.
Chunk initializationChunk = newInitializationChunk(pendingInitializationUri, pendingIndexUri, Chunk initializationChunk = newInitializationChunk(pendingInitializationUri, pendingIndexUri,
selectedRepresentation, extractorWrapper, dataSource, periodHolder.manifestIndex, selectedRepresentation, extractorWrapper, dataSource, periodHolder.localIndex,
evaluation.trigger); evaluation.trigger);
lastChunkWasInitialization = true; lastChunkWasInitialization = true;
out.chunk = initializationChunk; out.chunk = initializationChunk;
return; return;
} }
if (startingNewPeriod) { int segmentNum = queue.isEmpty() ? representationHolder.getSegmentNum(seekPositionUs)
if (queue.isEmpty()) { : startingNewPeriod ? representationHolder.getFirstAvailableSegmentNum()
// when starting a new period (or beginning playback for the first time), the segment : queue.get(out.queueSize - 1).chunkIndex + 1;
// 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();
}
}
Chunk nextMediaChunk = newMediaChunk(periodHolder, representationHolder, dataSource, Chunk nextMediaChunk = newMediaChunk(periodHolder, representationHolder, dataSource,
mediaFormat, segmentNum, evaluation.trigger); mediaFormat, segmentNum, evaluation.trigger);
lastChunkWasInitialization = false; lastChunkWasInitialization = false;
@ -534,8 +526,7 @@ public class DashChunkSource implements ChunkSource {
if (initializationChunk.hasSeekMap()) { if (initializationChunk.hasSeekMap()) {
representationHolder.segmentIndex = new DashWrappingSegmentIndex( representationHolder.segmentIndex = new DashWrappingSegmentIndex(
(ChunkIndex) initializationChunk.getSeekMap(), (ChunkIndex) initializationChunk.getSeekMap(),
initializationChunk.dataSpec.uri.toString(), initializationChunk.dataSpec.uri.toString());
periodHolder.startTimeUs);
} }
// The null check avoids overwriting drmInitData obtained from the manifest with drmInitData // 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, return new SingleSampleMediaChunk(dataSource, dataSpec, Chunk.TRIGGER_INITIAL,
representation.format, startTimeUs, endTimeUs, segmentNum, isLastSegment, representation.format, startTimeUs, endTimeUs, segmentNum, isLastSegment,
MediaFormat.createTextFormat(MimeTypes.TEXT_VTT, MediaFormat.NO_VALUE, MediaFormat.createTextFormat(MimeTypes.TEXT_VTT, MediaFormat.NO_VALUE,
representation.format.language), null, periodHolder.manifestIndex); representation.format.language), null, periodHolder.localIndex);
} else { } else {
boolean isMediaFormatFinal = (mediaFormat != null); boolean isMediaFormatFinal = (mediaFormat != null);
return new ContainerMediaChunk(dataSource, dataSpec, trigger, representation.format, return new ContainerMediaChunk(dataSource, dataSpec, trigger, representation.format,
startTimeUs, endTimeUs, segmentNum, isLastSegment, sampleOffsetUs, startTimeUs, endTimeUs, segmentNum, isLastSegment, sampleOffsetUs,
representationHolder.extractorWrapper, mediaFormat, maxWidth, maxHeight, drmInitData, 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, private static DrmInitData getDrmInitData(MediaPresentationDescription manifest,
int adaptationSetIndex) { 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) String drmInitMimeType = mimeTypeIsWebm(adaptationSet.representations.get(0).format.mimeType)
? MimeTypes.VIDEO_WEBM : MimeTypes.VIDEO_MP4; ? MimeTypes.VIDEO_WEBM : MimeTypes.VIDEO_MP4;
if (adaptationSet.contentProtections.isEmpty()) { if (adaptationSet.contentProtections.isEmpty()) {
@ -644,13 +635,11 @@ public class DashChunkSource implements ChunkSource {
} }
} }
private static MediaPresentationDescription buildManifest(List<Representation> representations) { private static MediaPresentationDescription buildManifest(long durationMs,
Representation firstRepresentation = representations.get(0); List<Representation> representations) {
AdaptationSet adaptationSet = new AdaptationSet(0, AdaptationSet.TYPE_UNKNOWN, representations); AdaptationSet adaptationSet = new AdaptationSet(0, AdaptationSet.TYPE_UNKNOWN, representations);
Period period = new Period(null, firstRepresentation.periodStartMs, Period period = new Period(null, 0, Collections.singletonList(adaptationSet));
firstRepresentation.periodDurationMs, Collections.singletonList(adaptationSet)); return new MediaPresentationDescription(-1, durationMs, -1, false, -1, -1, null, null,
long duration = firstRepresentation.periodDurationMs - firstRepresentation.periodStartMs;
return new MediaPresentationDescription(-1, duration, -1, false, -1, -1, null, null,
Collections.singletonList(period)); Collections.singletonList(period));
} }
@ -672,21 +661,19 @@ public class DashChunkSource implements ChunkSource {
} }
private void processManifest(MediaPresentationDescription manifest) { private void processManifest(MediaPresentationDescription manifest) {
List<Period> newPeriods = manifest.periods;
// Remove old periods. // Remove old periods.
Period firstPeriod = newPeriods.get(0); Period firstPeriod = manifest.getPeriod(0);
while (periodHolders.size() > 0 while (periodHolders.size() > 0
&& periodHolders.valueAt(0).startTimeUs < firstPeriod.startMs * 1000) { && periodHolders.valueAt(0).startTimeUs < firstPeriod.startMs * 1000) {
PeriodHolder periodHolder = periodHolders.valueAt(0); PeriodHolder periodHolder = periodHolders.valueAt(0);
// TODO: Use periodHolders.removeAt(0) if the minimum API level is ever increased to 11. // 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. // Update existing periods.
try { try {
for (int i = 0; i < periodHolders.size(); i++) { for (int i = 0; i < periodHolders.size(); i++) {
periodHolders.valueAt(i).updatePeriod(newPeriods.get(i)); periodHolders.valueAt(i).updatePeriod(manifest, i);
} }
} catch (BehindLiveWindowException e) { } catch (BehindLiveWindowException e) {
fatalError = e; fatalError = e;
@ -694,9 +681,8 @@ public class DashChunkSource implements ChunkSource {
} }
// Add new periods. // Add new periods.
for (int i = periodHolders.size(); i < newPeriods.size(); i++) { for (int i = periodHolders.size(); i < manifest.getPeriodCount(); i++) {
Period period = newPeriods.get(i); PeriodHolder periodHolder = new PeriodHolder(periodHolderNextIndex, manifest, i,
PeriodHolder periodHolder = new PeriodHolder(periodHolderNextIndex, period,
adaptationSetIndex, representationIndices); adaptationSetIndex, representationIndices);
periodHolders.put(periodHolderNextIndex, periodHolder); periodHolders.put(periodHolderNextIndex, periodHolder);
periodHolderNextIndex++; periodHolderNextIndex++;
@ -725,7 +711,7 @@ public class DashChunkSource implements ChunkSource {
long maxEndPositionUs = lastPeriod.isIndexUnbounded() ? Long.MAX_VALUE long maxEndPositionUs = lastPeriod.isIndexUnbounded() ? Long.MAX_VALUE
: lastPeriod.getAvailableEndTimeUs(); : lastPeriod.getAvailableEndTimeUs();
long elapsedRealtimeAtZeroUs = (systemClock.elapsedRealtime() * 1000) long elapsedRealtimeAtZeroUs = (systemClock.elapsedRealtime() * 1000)
- (nowUs - currentManifest.availabilityStartTime * 1000); - (nowUs - (currentManifest.availabilityStartTime * 1000));
long timeShiftBufferDepthUs = currentManifest.timeShiftBufferDepth == -1 ? -1 long timeShiftBufferDepthUs = currentManifest.timeShiftBufferDepth == -1 ? -1
: currentManifest.timeShiftBufferDepth * 1000; : currentManifest.timeShiftBufferDepth * 1000;
return new DynamicTimeRange(minStartPositionUs, maxEndPositionUs, elapsedRealtimeAtZeroUs, return new DynamicTimeRange(minStartPositionUs, maxEndPositionUs, elapsedRealtimeAtZeroUs,
@ -751,58 +737,76 @@ public class DashChunkSource implements ChunkSource {
public DashSegmentIndex segmentIndex; public DashSegmentIndex segmentIndex;
public MediaFormat mediaFormat; public MediaFormat mediaFormat;
private final long periodStartTimeUs;
private long periodDurationUs;
private int segmentNumShift; private int segmentNumShift;
public RepresentationHolder(Representation representation, public RepresentationHolder(long periodStartTimeUs, long periodDurationUs,
ChunkExtractorWrapper extractorWrapper) { Representation representation, ChunkExtractorWrapper extractorWrapper) {
this.periodStartTimeUs = periodStartTimeUs;
this.periodDurationUs = periodDurationUs;
this.representation = representation; this.representation = representation;
this.extractorWrapper = extractorWrapper; this.extractorWrapper = extractorWrapper;
this.segmentIndex = representation.getIndex(); segmentIndex = representation.getIndex();
} }
public void updateRepresentation(Representation newRepresentation) public void updateRepresentation(long newPeriodDurationUs, Representation newRepresentation)
throws BehindLiveWindowException{ throws BehindLiveWindowException{
DashSegmentIndex oldIndex = representation.getIndex(); DashSegmentIndex oldIndex = representation.getIndex();
DashSegmentIndex newIndex = newRepresentation.getIndex(); DashSegmentIndex newIndex = newRepresentation.getIndex();
periodDurationUs = newPeriodDurationUs;
representation = newRepresentation; representation = newRepresentation;
if (newIndex == null) { if (oldIndex == null) {
// Segment numbers cannot shift if the index isn't defined by the manifest.
return; return;
} }
segmentIndex = newIndex; 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) long oldIndexEndTimeUs = oldIndex.getTimeUs(oldIndexLastSegmentNum)
+ oldIndex.getDurationUs(oldIndexLastSegmentNum); + oldIndex.getDurationUs(oldIndexLastSegmentNum, periodDurationUs);
int newIndexFirstSegmentNum = newIndex.getFirstSegmentNum(); int newIndexFirstSegmentNum = newIndex.getFirstSegmentNum();
long newIndexStartTimeUs = newIndex.getTimeUs(newIndexFirstSegmentNum); long newIndexStartTimeUs = newIndex.getTimeUs(newIndexFirstSegmentNum);
if (oldIndexEndTimeUs == newIndexStartTimeUs) { if (oldIndexEndTimeUs == newIndexStartTimeUs) {
// The new manifest continues where the old one ended, with no overlap. // 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) { } else if (oldIndexEndTimeUs < newIndexStartTimeUs) {
// There's a gap between the old manifest and the new one which means we've slipped // 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. // behind the live window and can't proceed.
throw new BehindLiveWindowException(); throw new BehindLiveWindowException();
} else { } else {
// The new manifest overlaps with the old one. // The new manifest overlaps with the old one.
segmentNumShift += oldIndex.getSegmentNum(newIndexStartTimeUs) - newIndexFirstSegmentNum; segmentNumShift += oldIndex.getSegmentNum(newIndexStartTimeUs, periodDurationUs)
- newIndexFirstSegmentNum;
} }
} }
public int getSegmentNum(long positionUs) { public int getSegmentNum(long positionUs) {
return segmentIndex.getSegmentNum(positionUs) + segmentNumShift; return segmentIndex.getSegmentNum(positionUs - periodStartTimeUs, periodDurationUs)
+ segmentNumShift;
} }
public long getSegmentStartTimeUs(int segmentNum) { public long getSegmentStartTimeUs(int segmentNum) {
return segmentIndex.getTimeUs(segmentNum - segmentNumShift); return segmentIndex.getTimeUs(segmentNum - segmentNumShift) + periodStartTimeUs;
} }
public long getSegmentEndTimeUs(int segmentNum) { public long getSegmentEndTimeUs(int segmentNum) {
return getSegmentStartTimeUs(segmentNum) return getSegmentStartTimeUs(segmentNum)
+ segmentIndex.getDurationUs(segmentNum - segmentNumShift); + segmentIndex.getDurationUs(segmentNum - segmentNumShift, periodDurationUs);
} }
public boolean isLastSegment(int segmentNum) { 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() { public int getFirstAvailableSegmentNum() {
@ -817,7 +821,7 @@ public class DashChunkSource implements ChunkSource {
private static final class PeriodHolder { private static final class PeriodHolder {
public final int manifestIndex; public final int localIndex;
public final long startTimeUs; public final long startTimeUs;
public final long durationUs; public final long durationUs;
public final String mimeType; public final String mimeType;
@ -834,12 +838,16 @@ public class DashChunkSource implements ChunkSource {
private long availableStartTimeUs; private long availableStartTimeUs;
private long availableEndTimeUs; private long availableEndTimeUs;
public PeriodHolder(int manifestIndex, Period period, int adaptationSetIndex, public PeriodHolder(int localIndex, MediaPresentationDescription manifest,
int[] representationIndices) { int manifestIndex, int adaptationSetIndex, int[] representationIndices) {
this.manifestIndex = manifestIndex; this.localIndex = localIndex;
this.adaptationSetIndex = adaptationSetIndex; this.adaptationSetIndex = adaptationSetIndex;
this.representationIndices = representationIndices; this.representationIndices = representationIndices;
Period period = manifest.getPeriod(manifestIndex);
startTimeUs = period.startMs * 1000;
durationUs = getPeriodDurationUs(manifest, manifestIndex);
List<Representation> periodRepresentations = List<Representation> periodRepresentations =
period.adaptationSets.get(adaptationSetIndex).representations; period.adaptationSets.get(adaptationSetIndex).representations;
int representationCount = representationIndices != null ? representationIndices.length int representationCount = representationIndices != null ? representationIndices.length
@ -859,34 +867,31 @@ public class DashChunkSource implements ChunkSource {
maxHeight = Math.max(formats[i].height, maxHeight); maxHeight = Math.max(formats[i].height, maxHeight);
Extractor extractor = mimeTypeIsWebm(formats[i].mimeType) ? new WebmExtractor() Extractor extractor = mimeTypeIsWebm(formats[i].mimeType) ? new WebmExtractor()
: new FragmentedMp4Extractor(); : new FragmentedMp4Extractor();
RepresentationHolder representationHolder = RepresentationHolder representationHolder = new RepresentationHolder(startTimeUs,
new RepresentationHolder(representation, new ChunkExtractorWrapper(extractor)); durationUs, representation, new ChunkExtractorWrapper(extractor));
representationHolders.put(formats[i].id, representationHolder); representationHolders.put(formats[i].id, representationHolder);
} }
this.maxWidth = maxWidth; this.maxWidth = maxWidth;
this.maxHeight = maxHeight; this.maxHeight = maxHeight;
this.mimeType = mimeType; 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()); Arrays.sort(formats, new DecreasingBandwidthComparator());
updateRepresentationIndependentProperties(); 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<Representation> representations = List<Representation> representations =
period.adaptationSets.get(adaptationSetIndex).representations; period.adaptationSets.get(adaptationSetIndex).representations;
int representationCount = formats.length; int representationCount = formats.length;
for (int i = 0; i < representationCount; i++) { for (int i = 0; i < representationCount; i++) {
int representationIndex = representationIndices != null ? representationIndices[i] : i; int representationIndex = representationIndices != null ? representationIndices[i] : i;
Representation representation = representations.get(representationIndex); Representation representation = representations.get(representationIndex);
representationHolders.get(representation.format.id).updateRepresentation(representation); representationHolders.get(representation.format.id).updateRepresentation(durationUs,
representation);
} }
updateRepresentationIndependentProperties(); updateRepresentationIndependentProperties();
} }
@ -915,13 +920,14 @@ public class DashChunkSource implements ChunkSource {
Representation representation = representationHolders.get(formats[0].id).representation; Representation representation = representationHolders.get(formats[0].id).representation;
DashSegmentIndex segmentIndex = representation.getIndex(); DashSegmentIndex segmentIndex = representation.getIndex();
if (segmentIndex != null) { if (segmentIndex != null) {
int lastSegmentNum = segmentIndex.getLastSegmentNum(); int firstSegmentNum = segmentIndex.getFirstSegmentNum();
int lastSegmentNum = segmentIndex.getLastSegmentNum(durationUs);
indexIsUnbounded = lastSegmentNum == DashSegmentIndex.INDEX_UNBOUNDED; indexIsUnbounded = lastSegmentNum == DashSegmentIndex.INDEX_UNBOUNDED;
indexIsExplicit = segmentIndex.isExplicit(); indexIsExplicit = segmentIndex.isExplicit();
availableStartTimeUs = segmentIndex.getTimeUs(segmentIndex.getFirstSegmentNum()); availableStartTimeUs = startTimeUs + segmentIndex.getTimeUs(firstSegmentNum);
if (!indexIsUnbounded) { if (!indexIsUnbounded) {
availableEndTimeUs = segmentIndex.getTimeUs(lastSegmentNum) availableEndTimeUs = startTimeUs + segmentIndex.getTimeUs(lastSegmentNum)
+ segmentIndex.getDurationUs(lastSegmentNum); + segmentIndex.getDurationUs(lastSegmentNum, durationUs);
} }
} else { } else {
indexIsUnbounded = false; 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;
}
}
} }
} }

View File

@ -15,6 +15,7 @@
*/ */
package com.google.android.exoplayer.dash; package com.google.android.exoplayer.dash;
import com.google.android.exoplayer.C;
import com.google.android.exoplayer.dash.mpd.RangedUri; import com.google.android.exoplayer.dash.mpd.RangedUri;
/** /**
@ -24,20 +25,22 @@ import com.google.android.exoplayer.dash.mpd.RangedUri;
*/ */
public interface DashSegmentIndex { 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. * Returns the segment number of the segment containing a given media time.
* <p> * <p>
* If the given media time is outside the range of the index, then the returned segment number is * 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 * 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 * first segment) or {@link #getLastSegmentNum(long)} (if the given media time is later then the
* of the last segment). * end of the last segment).
* *
* @param timeUs The time in microseconds. * @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. * @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. * Returns the start time of a segment.
@ -51,9 +54,11 @@ public interface DashSegmentIndex {
* Returns the duration of a segment. * Returns the duration of a segment.
* *
* @param segmentNum The segment number. * @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. * @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. * 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}. * Returns the segment number of the last segment, or {@link #INDEX_UNBOUNDED}.
* <p> * <p>
* An unbounded index occurs if a live stream manifest uses SegmentTemplate elements without a * An unbounded index occurs if a dynamic manifest uses SegmentTemplate elements without a
* SegmentTimeline element. In this case the manifest can be used to derive information about * SegmentTimeline element, and if the period duration is not yet known. In this case the caller
* segments arbitrarily far into the future. This means that the manifest does not need to be * must manually determine the window of currently available segments.
* refreshed as frequently (if at all) during playback, however it is necessary for a player to
* manually calculate 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}. * @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. * Returns true if segments are defined explicitly by the index.

View File

@ -26,17 +26,14 @@ import com.google.android.exoplayer.extractor.ChunkIndex;
private final ChunkIndex chunkIndex; private final ChunkIndex chunkIndex;
private final String uri; private final String uri;
private final long startTimeUs;
/** /**
* @param chunkIndex The {@link ChunkIndex} to wrap. * @param chunkIndex The {@link ChunkIndex} to wrap.
* @param uri The URI where the data is located. * @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.chunkIndex = chunkIndex;
this.uri = uri; this.uri = uri;
this.startTimeUs = startTimeUs;
} }
@Override @Override
@ -45,17 +42,17 @@ import com.google.android.exoplayer.extractor.ChunkIndex;
} }
@Override @Override
public int getLastSegmentNum() { public int getLastSegmentNum(long periodDurationUs) {
return chunkIndex.length - 1; return chunkIndex.length - 1;
} }
@Override @Override
public long getTimeUs(int segmentNum) { public long getTimeUs(int segmentNum) {
return chunkIndex.timesUs[segmentNum] + startTimeUs; return chunkIndex.timesUs[segmentNum];
} }
@Override @Override
public long getDurationUs(int segmentNum) { public long getDurationUs(int segmentNum, long periodDurationUs) {
return chunkIndex.durationsUs[segmentNum]; return chunkIndex.durationsUs[segmentNum];
} }
@ -65,8 +62,8 @@ import com.google.android.exoplayer.extractor.ChunkIndex;
} }
@Override @Override
public int getSegmentNum(long timeUs) { public int getSegmentNum(long timeUs, long periodDurationUs) {
return chunkIndex.getChunkIndex(timeUs - startTimeUs); return chunkIndex.getChunkIndex(timeUs);
} }
@Override @Override

View File

@ -22,34 +22,28 @@ import com.google.android.exoplayer.dash.DashSegmentIndex;
*/ */
/* package */ final class DashSingleSegmentIndex implements DashSegmentIndex { /* package */ final class DashSingleSegmentIndex implements DashSegmentIndex {
private final long startTimeUs;
private final long durationUs;
private final RangedUri uri; 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. * @param uri A {@link RangedUri} defining the location of the segment data.
*/ */
public DashSingleSegmentIndex(long startTimeUs, long durationUs, RangedUri uri) { public DashSingleSegmentIndex(RangedUri uri) {
this.startTimeUs = startTimeUs;
this.durationUs = durationUs;
this.uri = uri; this.uri = uri;
} }
@Override @Override
public int getSegmentNum(long timeUs) { public int getSegmentNum(long timeUs, long periodDurationUs) {
return 0; return 0;
} }
@Override @Override
public long getTimeUs(int segmentNum) { public long getTimeUs(int segmentNum) {
return startTimeUs; return 0;
} }
@Override @Override
public long getDurationUs(int segmentNum) { public long getDurationUs(int segmentNum, long periodDurationUs) {
return durationUs; return periodDurationUs;
} }
@Override @Override
@ -63,7 +57,7 @@ import com.google.android.exoplayer.dash.DashSegmentIndex;
} }
@Override @Override
public int getLastSegmentNum() { public int getLastSegmentNum(long periodDurationUs) {
return 0; return 0;
} }

View File

@ -41,7 +41,7 @@ public class MediaPresentationDescription implements RedirectingManifest {
public final String location; public final String location;
public final List<Period> periods; private final List<Period> periods;
public MediaPresentationDescription(long availabilityStartTime, long duration, long minBufferTime, public MediaPresentationDescription(long availabilityStartTime, long duration, long minBufferTime,
boolean dynamic, long minUpdatePeriod, long timeShiftBufferDepth, UtcTimingElement utcTiming, boolean dynamic, long minUpdatePeriod, long timeShiftBufferDepth, UtcTimingElement utcTiming,
@ -54,12 +54,26 @@ public class MediaPresentationDescription implements RedirectingManifest {
this.timeShiftBufferDepth = timeShiftBufferDepth; this.timeShiftBufferDepth = timeShiftBufferDepth;
this.utcTiming = utcTiming; this.utcTiming = utcTiming;
this.location = location; this.location = location;
this.periods = Collections.unmodifiableList(periods); this.periods = periods == null ? Collections.<Period>emptyList() : periods;
} }
@Override @Override
public String getNextManifestUri() { public final String getNextManifestUri() {
return location; 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;
}
} }

View File

@ -30,6 +30,7 @@ import com.google.android.exoplayer.util.Util;
import android.text.TextUtils; import android.text.TextUtils;
import android.util.Base64; import android.util.Base64;
import android.util.Pair;
import org.xml.sax.helpers.DefaultHandler; import org.xml.sax.helpers.DefaultHandler;
import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParser;
@ -114,16 +115,32 @@ public class MediaPresentationDescriptionParser extends DefaultHandler
String location = null; String location = null;
List<Period> periods = new ArrayList<>(); List<Period> periods = new ArrayList<>();
long nextPeriodStartMs = dynamic ? -1 : 0;
boolean seenEarlyAccessPeriod = false;
do { do {
xpp.next(); xpp.next();
if (isStartTag(xpp, "BaseURL")) { if (isStartTag(xpp, "BaseURL")) {
baseUrl = parseBaseUrl(xpp, baseUrl); baseUrl = parseBaseUrl(xpp, baseUrl);
} else if (isStartTag(xpp, "UTCTiming")) { } else if (isStartTag(xpp, "UTCTiming")) {
utcTiming = parseUtcTiming(xpp); utcTiming = parseUtcTiming(xpp);
} else if (isStartTag(xpp, "Period")) {
periods.add(parsePeriod(xpp, baseUrl, durationMs));
} else if (isStartTag(xpp, "Location")) { } else if (isStartTag(xpp, "Location")) {
location = xpp.nextText(); location = xpp.nextText();
} else if (isStartTag(xpp, "Period") && !seenEarlyAccessPeriod) {
Pair<Period, Long> 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")); } while (!isEndTag(xpp, "MPD"));
@ -149,11 +166,11 @@ public class MediaPresentationDescriptionParser extends DefaultHandler
return new UtcTimingElement(schemeIdUri, value); return new UtcTimingElement(schemeIdUri, value);
} }
protected Period parsePeriod(XmlPullParser xpp, String baseUrl, long mpdDurationMs) protected Pair<Period, Long> parsePeriod(XmlPullParser xpp, String baseUrl, long defaultStartMs)
throws XmlPullParserException, IOException { throws XmlPullParserException, IOException {
String id = xpp.getAttributeValue(null, "id"); String id = xpp.getAttributeValue(null, "id");
long startMs = parseDuration(xpp, "start", 0); long startMs = parseDuration(xpp, "start", defaultStartMs);
long durationMs = parseDuration(xpp, "duration", mpdDurationMs); long durationMs = parseDuration(xpp, "duration", -1);
SegmentBase segmentBase = null; SegmentBase segmentBase = null;
List<AdaptationSet> adaptationSets = new ArrayList<>(); List<AdaptationSet> adaptationSets = new ArrayList<>();
do { do {
@ -161,29 +178,27 @@ public class MediaPresentationDescriptionParser extends DefaultHandler
if (isStartTag(xpp, "BaseURL")) { if (isStartTag(xpp, "BaseURL")) {
baseUrl = parseBaseUrl(xpp, baseUrl); baseUrl = parseBaseUrl(xpp, baseUrl);
} else if (isStartTag(xpp, "AdaptationSet")) { } else if (isStartTag(xpp, "AdaptationSet")) {
adaptationSets.add(parseAdaptationSet(xpp, baseUrl, startMs, durationMs, adaptationSets.add(parseAdaptationSet(xpp, baseUrl, segmentBase));
segmentBase));
} else if (isStartTag(xpp, "SegmentBase")) { } else if (isStartTag(xpp, "SegmentBase")) {
segmentBase = parseSegmentBase(xpp, baseUrl, null); segmentBase = parseSegmentBase(xpp, baseUrl, null);
} else if (isStartTag(xpp, "SegmentList")) { } else if (isStartTag(xpp, "SegmentList")) {
segmentBase = parseSegmentList(xpp, baseUrl, null, durationMs); segmentBase = parseSegmentList(xpp, baseUrl, null);
} else if (isStartTag(xpp, "SegmentTemplate")) { } else if (isStartTag(xpp, "SegmentTemplate")) {
segmentBase = parseSegmentTemplate(xpp, baseUrl, null, durationMs); segmentBase = parseSegmentTemplate(xpp, baseUrl, null);
} }
} while (!isEndTag(xpp, "Period")); } while (!isEndTag(xpp, "Period"));
return buildPeriod(id, startMs, durationMs, adaptationSets); return Pair.create(buildPeriod(id, startMs, adaptationSets), durationMs);
} }
protected Period buildPeriod( protected Period buildPeriod(String id, long startMs, List<AdaptationSet> adaptationSets) {
String id, long startMs, long durationMs, List<AdaptationSet> adaptationSets) { return new Period(id, startMs, adaptationSets);
return new Period(id, startMs, durationMs, adaptationSets);
} }
// AdaptationSet parsing. // AdaptationSet parsing.
protected AdaptationSet parseAdaptationSet(XmlPullParser xpp, String baseUrl, long periodStartMs, protected AdaptationSet parseAdaptationSet(XmlPullParser xpp, String baseUrl,
long periodDurationMs, SegmentBase segmentBase) throws XmlPullParserException, IOException { SegmentBase segmentBase) throws XmlPullParserException, IOException {
int id = parseInt(xpp, "id", -1); int id = parseInt(xpp, "id", -1);
int contentType = parseContentType(xpp); int contentType = parseContentType(xpp);
@ -208,9 +223,9 @@ public class MediaPresentationDescriptionParser extends DefaultHandler
language = checkLanguageConsistency(language, xpp.getAttributeValue(null, "lang")); language = checkLanguageConsistency(language, xpp.getAttributeValue(null, "lang"));
contentType = checkContentTypeConsistency(contentType, parseContentType(xpp)); contentType = checkContentTypeConsistency(contentType, parseContentType(xpp));
} else if (isStartTag(xpp, "Representation")) { } else if (isStartTag(xpp, "Representation")) {
Representation representation = parseRepresentation(xpp, baseUrl, periodStartMs, Representation representation = parseRepresentation(xpp, baseUrl, mimeType, codecs, width,
periodDurationMs, mimeType, codecs, width, height, frameRate, audioChannels, height, frameRate, audioChannels, audioSamplingRate, language, segmentBase,
audioSamplingRate, language, segmentBase, contentProtectionsBuilder); contentProtectionsBuilder);
contentProtectionsBuilder.endRepresentation(); contentProtectionsBuilder.endRepresentation();
contentType = checkContentTypeConsistency(contentType, getContentType(representation)); contentType = checkContentTypeConsistency(contentType, getContentType(representation));
representations.add(representation); representations.add(representation);
@ -219,10 +234,9 @@ public class MediaPresentationDescriptionParser extends DefaultHandler
} else if (isStartTag(xpp, "SegmentBase")) { } else if (isStartTag(xpp, "SegmentBase")) {
segmentBase = parseSegmentBase(xpp, baseUrl, (SingleSegmentBase) segmentBase); segmentBase = parseSegmentBase(xpp, baseUrl, (SingleSegmentBase) segmentBase);
} else if (isStartTag(xpp, "SegmentList")) { } else if (isStartTag(xpp, "SegmentList")) {
segmentBase = parseSegmentList(xpp, baseUrl, (SegmentList) segmentBase, periodDurationMs); segmentBase = parseSegmentList(xpp, baseUrl, (SegmentList) segmentBase);
} else if (isStartTag(xpp, "SegmentTemplate")) { } else if (isStartTag(xpp, "SegmentTemplate")) {
segmentBase = parseSegmentTemplate(xpp, baseUrl, (SegmentTemplate) segmentBase, segmentBase = parseSegmentTemplate(xpp, baseUrl, (SegmentTemplate) segmentBase);
periodDurationMs);
} else if (isStartTag(xpp)) { } else if (isStartTag(xpp)) {
parseAdaptationSetChild(xpp); parseAdaptationSetChild(xpp);
} }
@ -311,9 +325,8 @@ public class MediaPresentationDescriptionParser extends DefaultHandler
// Representation parsing. // Representation parsing.
protected Representation parseRepresentation(XmlPullParser xpp, String baseUrl, protected Representation parseRepresentation(XmlPullParser xpp, String baseUrl,
long periodStartMs, long periodDurationMs, String adaptationSetMimeType, String adaptationSetMimeType, String adaptationSetCodecs, int adaptationSetWidth,
String adaptationSetCodecs, int adaptationSetWidth, int adaptationSetHeight, int adaptationSetHeight, float adaptationSetFrameRate, int adaptationSetAudioChannels,
float adaptationSetFrameRate, int adaptationSetAudioChannels,
int adaptationSetAudioSamplingRate, String adaptationSetLanguage, SegmentBase segmentBase, int adaptationSetAudioSamplingRate, String adaptationSetLanguage, SegmentBase segmentBase,
ContentProtectionsBuilder contentProtectionsBuilder) ContentProtectionsBuilder contentProtectionsBuilder)
throws XmlPullParserException, IOException { throws XmlPullParserException, IOException {
@ -338,10 +351,9 @@ public class MediaPresentationDescriptionParser extends DefaultHandler
} else if (isStartTag(xpp, "SegmentBase")) { } else if (isStartTag(xpp, "SegmentBase")) {
segmentBase = parseSegmentBase(xpp, baseUrl, (SingleSegmentBase) segmentBase); segmentBase = parseSegmentBase(xpp, baseUrl, (SingleSegmentBase) segmentBase);
} else if (isStartTag(xpp, "SegmentList")) { } else if (isStartTag(xpp, "SegmentList")) {
segmentBase = parseSegmentList(xpp, baseUrl, (SegmentList) segmentBase, periodDurationMs); segmentBase = parseSegmentList(xpp, baseUrl, (SegmentList) segmentBase);
} else if (isStartTag(xpp, "SegmentTemplate")) { } else if (isStartTag(xpp, "SegmentTemplate")) {
segmentBase = parseSegmentTemplate(xpp, baseUrl, (SegmentTemplate) segmentBase, segmentBase = parseSegmentTemplate(xpp, baseUrl, (SegmentTemplate) segmentBase);
periodDurationMs);
} else if (isStartTag(xpp, "ContentProtection")) { } else if (isStartTag(xpp, "ContentProtection")) {
contentProtectionsBuilder.addRepresentationProtection(parseContentProtection(xpp)); contentProtectionsBuilder.addRepresentationProtection(parseContentProtection(xpp));
} }
@ -349,7 +361,7 @@ public class MediaPresentationDescriptionParser extends DefaultHandler
Format format = buildFormat(id, mimeType, width, height, frameRate, audioChannels, Format format = buildFormat(id, mimeType, width, height, frameRate, audioChannels,
audioSamplingRate, bandwidth, language, codecs); audioSamplingRate, bandwidth, language, codecs);
return buildRepresentation(periodStartMs, periodDurationMs, contentId, -1, format, return buildRepresentation(contentId, -1, format,
segmentBase != null ? segmentBase : new SingleSegmentBase(baseUrl)); segmentBase != null ? segmentBase : new SingleSegmentBase(baseUrl));
} }
@ -359,10 +371,9 @@ public class MediaPresentationDescriptionParser extends DefaultHandler
bandwidth, language, codecs); bandwidth, language, codecs);
} }
protected Representation buildRepresentation(long periodStartMs, long periodDurationMs, protected Representation buildRepresentation(String contentId, int revisionId, Format format,
String contentId, int revisionId, Format format, SegmentBase segmentBase) { SegmentBase segmentBase) {
return Representation.newInstance(periodStartMs, periodDurationMs, contentId, revisionId, return Representation.newInstance(contentId, revisionId, format, segmentBase);
format, segmentBase);
} }
// SegmentBase, SegmentList and SegmentTemplate parsing. // SegmentBase, SegmentList and SegmentTemplate parsing.
@ -401,8 +412,8 @@ public class MediaPresentationDescriptionParser extends DefaultHandler
indexStart, indexLength); indexStart, indexLength);
} }
protected SegmentList parseSegmentList(XmlPullParser xpp, String baseUrl, SegmentList parent, protected SegmentList parseSegmentList(XmlPullParser xpp, String baseUrl, SegmentList parent)
long periodDurationMs) throws XmlPullParserException, IOException { throws XmlPullParserException, IOException {
long timescale = parseLong(xpp, "timescale", parent != null ? parent.timescale : 1); long timescale = parseLong(xpp, "timescale", parent != null ? parent.timescale : 1);
long presentationTimeOffset = parseLong(xpp, "presentationTimeOffset", long presentationTimeOffset = parseLong(xpp, "presentationTimeOffset",
@ -434,19 +445,19 @@ public class MediaPresentationDescriptionParser extends DefaultHandler
segments = segments != null ? segments : parent.mediaSegments; segments = segments != null ? segments : parent.mediaSegments;
} }
return buildSegmentList(initialization, timescale, presentationTimeOffset, periodDurationMs, return buildSegmentList(initialization, timescale, presentationTimeOffset,
startNumber, duration, timeline, segments); startNumber, duration, timeline, segments);
} }
protected SegmentList buildSegmentList(RangedUri initialization, long timescale, protected SegmentList buildSegmentList(RangedUri initialization, long timescale,
long presentationTimeOffset, long periodDurationMs, int startNumber, long duration, long presentationTimeOffset, int startNumber, long duration,
List<SegmentTimelineElement> timeline, List<RangedUri> segments) { List<SegmentTimelineElement> timeline, List<RangedUri> segments) {
return new SegmentList(initialization, timescale, presentationTimeOffset, periodDurationMs, return new SegmentList(initialization, timescale, presentationTimeOffset,
startNumber, duration, timeline, segments); startNumber, duration, timeline, segments);
} }
protected SegmentTemplate parseSegmentTemplate(XmlPullParser xpp, String baseUrl, 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 timescale = parseLong(xpp, "timescale", parent != null ? parent.timescale : 1);
long presentationTimeOffset = parseLong(xpp, "presentationTimeOffset", long presentationTimeOffset = parseLong(xpp, "presentationTimeOffset",
@ -475,15 +486,15 @@ public class MediaPresentationDescriptionParser extends DefaultHandler
timeline = timeline != null ? timeline : parent.segmentTimeline; timeline = timeline != null ? timeline : parent.segmentTimeline;
} }
return buildSegmentTemplate(initialization, timescale, presentationTimeOffset, periodDurationMs, return buildSegmentTemplate(initialization, timescale, presentationTimeOffset,
startNumber, duration, timeline, initializationTemplate, mediaTemplate, baseUrl); startNumber, duration, timeline, initializationTemplate, mediaTemplate, baseUrl);
} }
protected SegmentTemplate buildSegmentTemplate(RangedUri initialization, long timescale, protected SegmentTemplate buildSegmentTemplate(RangedUri initialization, long timescale,
long presentationTimeOffset, long periodDurationMs, int startNumber, long duration, long presentationTimeOffset, int startNumber, long duration,
List<SegmentTimelineElement> timeline, UrlTemplate initializationTemplate, List<SegmentTimelineElement> timeline, UrlTemplate initializationTemplate,
UrlTemplate mediaTemplate, String baseUrl) { UrlTemplate mediaTemplate, String baseUrl) {
return new SegmentTemplate(initialization, timescale, presentationTimeOffset, periodDurationMs, return new SegmentTemplate(initialization, timescale, presentationTimeOffset,
startNumber, duration, timeline, initializationTemplate, mediaTemplate, baseUrl); startNumber, duration, timeline, initializationTemplate, mediaTemplate, baseUrl);
} }

View File

@ -33,11 +33,6 @@ public class Period {
*/ */
public final long startMs; 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. * The adaptation sets belonging to the period.
*/ */
@ -46,13 +41,11 @@ public class Period {
/** /**
* @param id The period identifier. May be null. * @param id The period identifier. May be null.
* @param start The start time of the period in milliseconds. * @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. * @param adaptationSets The adaptation sets belonging to the period.
*/ */
public Period(String id, long start, long duration, List<AdaptationSet> adaptationSets) { public Period(String id, long start, List<AdaptationSet> adaptationSets) {
this.id = id; this.id = id;
this.startMs = start; this.startMs = start;
this.durationMs = duration;
this.adaptationSets = Collections.unmodifiableList(adaptationSets); this.adaptationSets = Collections.unmodifiableList(adaptationSets);
} }

View File

@ -35,7 +35,6 @@ public abstract class Representation implements FormatWrapper {
* {@link #contentId}, which should uniquely identify that video. * {@link #contentId}, which should uniquely identify that video.
*/ */
public final String contentId; public final String contentId;
/** /**
* Identifies the revision of the content. * Identifies the revision of the content.
* <p> * <p>
@ -45,22 +44,10 @@ public abstract class Representation implements FormatWrapper {
* timestamp at which the media was encoded is often a suitable. * timestamp at which the media was encoded is often a suitable.
*/ */
public final long revisionId; public final long revisionId;
/** /**
* The format of the representation. * The format of the representation.
*/ */
public final Format format; 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. * 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. * 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 contentId Identifies the piece of content to which this representation belongs.
* @param revisionId Identifies the revision of the content. * @param revisionId Identifies the revision of the content.
* @param format The format of the representation. * @param format The format of the representation.
* @param segmentBase A segment base element for the representation. * @param segmentBase A segment base element for the representation.
* @return The constructed instance. * @return The constructed instance.
*/ */
public static Representation newInstance(long periodStartMs, long periodDurationMs, public static Representation newInstance(String contentId, long revisionId, Format format,
String contentId, long revisionId, Format format, SegmentBase segmentBase) { SegmentBase segmentBase) {
if (segmentBase instanceof SingleSegmentBase) { if (segmentBase instanceof SingleSegmentBase) {
return new SingleSegmentRepresentation(periodStartMs, periodDurationMs, contentId, revisionId, return new SingleSegmentRepresentation(contentId, revisionId, format,
format, (SingleSegmentBase) segmentBase, -1); (SingleSegmentBase) segmentBase, -1);
} else if (segmentBase instanceof MultiSegmentBase) { } else if (segmentBase instanceof MultiSegmentBase) {
return new MultiSegmentRepresentation(periodStartMs, periodDurationMs, contentId, revisionId, return new MultiSegmentRepresentation(contentId, revisionId, format,
format, (MultiSegmentBase) segmentBase); (MultiSegmentBase) segmentBase);
} else { } else {
throw new IllegalArgumentException("segmentBase must be of type SingleSegmentBase or " throw new IllegalArgumentException("segmentBase must be of type SingleSegmentBase or "
+ "MultiSegmentBase"); + "MultiSegmentBase");
} }
} }
private Representation(long periodStartMs, long periodDurationMs, String contentId, private Representation(String contentId, long revisionId, Format format,
long revisionId, Format format, SegmentBase segmentBase) { SegmentBase segmentBase) {
this.periodStartMs = periodStartMs;
this.periodDurationMs = periodDurationMs;
this.contentId = contentId; this.contentId = contentId;
this.revisionId = revisionId; this.revisionId = revisionId;
this.format = format; this.format = format;
@ -165,9 +147,6 @@ public abstract class Representation implements FormatWrapper {
private final DashSingleSegmentIndex segmentIndex; 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 contentId Identifies the piece of content to which this representation belongs.
* @param revisionId Identifies the revision of the content. * @param revisionId Identifies the revision of the content.
* @param format The format of the representation. * @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 indexEnd The offset of the last byte of index data.
* @param contentLength The content length, or -1 if unknown. * @param contentLength The content length, or -1 if unknown.
*/ */
public static SingleSegmentRepresentation newInstance(long periodStartMs, long periodDurationMs, public static SingleSegmentRepresentation newInstance(String contentId, long revisionId,
String contentId, long revisionId, Format format, String uri, long initializationStart, Format format, String uri, long initializationStart, long initializationEnd,
long initializationEnd, long indexStart, long indexEnd, long contentLength) { long indexStart, long indexEnd, long contentLength) {
RangedUri rangedUri = new RangedUri(uri, null, initializationStart, RangedUri rangedUri = new RangedUri(uri, null, initializationStart,
initializationEnd - initializationStart + 1); initializationEnd - initializationStart + 1);
SingleSegmentBase segmentBase = new SingleSegmentBase(rangedUri, 1, 0, uri, indexStart, SingleSegmentBase segmentBase = new SingleSegmentBase(rangedUri, 1, 0, uri, indexStart,
indexEnd - indexStart + 1); indexEnd - indexStart + 1);
return new SingleSegmentRepresentation(periodStartMs, periodDurationMs, contentId, revisionId, return new SingleSegmentRepresentation(contentId, revisionId,
format, segmentBase, contentLength); 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 contentId Identifies the piece of content to which this representation belongs.
* @param revisionId Identifies the revision of the content. * @param revisionId Identifies the revision of the content.
* @param format The format of the representation. * @param format The format of the representation.
* @param segmentBase The segment base underlying the representation. * @param segmentBase The segment base underlying the representation.
* @param contentLength The content length, or -1 if unknown. * @param contentLength The content length, or -1 if unknown.
*/ */
public SingleSegmentRepresentation(long periodStartMs, long periodDurationMs, String contentId, public SingleSegmentRepresentation(String contentId, long revisionId, Format format,
long revisionId, Format format, SingleSegmentBase segmentBase, long contentLength) { SingleSegmentBase segmentBase, long contentLength) {
super(periodStartMs, periodDurationMs, contentId, revisionId, format, segmentBase); super(contentId, revisionId, format, segmentBase);
this.uri = Uri.parse(segmentBase.uri); this.uri = Uri.parse(segmentBase.uri);
this.indexUri = segmentBase.getIndex(); this.indexUri = segmentBase.getIndex();
this.contentLength = contentLength; this.contentLength = contentLength;
// If we have an index uri then the index is defined externally, and we shouldn't return one // 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. // 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, segmentIndex = indexUri != null ? null
periodDurationMs * 1000, new RangedUri(segmentBase.uri, null, 0, -1)); : new DashSingleSegmentIndex(new RangedUri(segmentBase.uri, null, 0, contentLength));
} }
@Override @Override
@ -232,17 +208,14 @@ public abstract class Representation implements FormatWrapper {
private final MultiSegmentBase segmentBase; 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 contentId Identifies the piece of content to which this representation belongs.
* @param revisionId Identifies the revision of the content. * @param revisionId Identifies the revision of the content.
* @param format The format of the representation. * @param format The format of the representation.
* @param segmentBase The segment base underlying the representation. * @param segmentBase The segment base underlying the representation.
*/ */
public MultiSegmentRepresentation(long periodStartMs, long periodDurationMs, String contentId, public MultiSegmentRepresentation(String contentId, long revisionId, Format format,
long revisionId, Format format, MultiSegmentBase segmentBase) { MultiSegmentBase segmentBase) {
super(periodStartMs, periodDurationMs, contentId, revisionId, format, segmentBase); super(contentId, revisionId, format, segmentBase);
this.segmentBase = segmentBase; this.segmentBase = segmentBase;
} }
@ -264,18 +237,18 @@ public abstract class Representation implements FormatWrapper {
} }
@Override @Override
public int getSegmentNum(long timeUs) { public int getSegmentNum(long timeUs, long periodDurationUs) {
return segmentBase.getSegmentNum(timeUs - periodStartMs * 1000); return segmentBase.getSegmentNum(timeUs, periodDurationUs);
} }
@Override @Override
public long getTimeUs(int segmentIndex) { public long getTimeUs(int segmentIndex) {
return segmentBase.getSegmentTimeUs(segmentIndex) + periodStartMs * 1000; return segmentBase.getSegmentTimeUs(segmentIndex);
} }
@Override @Override
public long getDurationUs(int segmentIndex) { public long getDurationUs(int segmentIndex, long periodDurationUs) {
return segmentBase.getSegmentDurationUs(segmentIndex); return segmentBase.getSegmentDurationUs(segmentIndex, periodDurationUs);
} }
@Override @Override
@ -284,8 +257,8 @@ public abstract class Representation implements FormatWrapper {
} }
@Override @Override
public int getLastSegmentNum() { public int getLastSegmentNum(long periodDurationUs) {
return segmentBase.getLastSegmentNum(); return segmentBase.getLastSegmentNum(periodDurationUs);
} }
@Override @Override

View File

@ -112,7 +112,6 @@ public abstract class SegmentBase {
*/ */
public abstract static class MultiSegmentBase extends SegmentBase { public abstract static class MultiSegmentBase extends SegmentBase {
/* package */ final long periodDurationMs;
/* package */ final int startNumber; /* package */ final int startNumber;
/* package */ final long duration; /* package */ final long duration;
/* package */ final List<SegmentTimelineElement> segmentTimeline; /* package */ final List<SegmentTimelineElement> segmentTimeline;
@ -123,7 +122,6 @@ public abstract class SegmentBase {
* @param timescale The timescale in units per second. * @param timescale The timescale in units per second.
* @param presentationTimeOffset The presentation time offset. The value in seconds is the * @param presentationTimeOffset The presentation time offset. The value in seconds is the
* division of this value and {@code timescale}. * 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 startNumber The sequence number of the first segment.
* @param duration The duration of each segment in the case of fixed duration segments. The * @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 * value in seconds is the division of this value and {@code timescale}. If
@ -133,22 +131,20 @@ public abstract class SegmentBase {
* parameter. * parameter.
*/ */
public MultiSegmentBase(RangedUri initialization, long timescale, long presentationTimeOffset, public MultiSegmentBase(RangedUri initialization, long timescale, long presentationTimeOffset,
long periodDurationMs, int startNumber, long duration, int startNumber, long duration, List<SegmentTimelineElement> segmentTimeline) {
List<SegmentTimelineElement> segmentTimeline) {
super(initialization, timescale, presentationTimeOffset); super(initialization, timescale, presentationTimeOffset);
this.periodDurationMs = periodDurationMs;
this.startNumber = startNumber; this.startNumber = startNumber;
this.duration = duration; this.duration = duration;
this.segmentTimeline = segmentTimeline; 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(); final int firstSegmentNum = getFirstSegmentNum();
int lowIndex = firstSegmentNum; int lowIndex = firstSegmentNum;
int highIndex = getLastSegmentNum(); int highIndex = getLastSegmentNum(periodDurationUs);
if (segmentTimeline == null) { if (segmentTimeline == null) {
// All segments are of equal duration (with the possible exception of the last one). // All segments are of equal duration (with the possible exception of the last one).
long durationUs = (duration * C.MICROS_PER_SECOND) / timescale; 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) { if (segmentTimeline != null) {
long duration = segmentTimeline.get(sequenceNumber - startNumber).duration; long duration = segmentTimeline.get(sequenceNumber - startNumber).duration;
return (duration * C.MICROS_PER_SECOND) / timescale; return (duration * C.MICROS_PER_SECOND) / timescale;
} else { } else {
return sequenceNumber == getLastSegmentNum() return sequenceNumber == getLastSegmentNum(periodDurationUs)
? ((periodDurationMs * 1000) - getSegmentTimeUs(sequenceNumber)) ? (periodDurationUs - getSegmentTimeUs(sequenceNumber))
: ((duration * C.MICROS_PER_SECOND) / timescale); : ((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() * @see DashSegmentIndex#isExplicit()
@ -244,7 +240,6 @@ public abstract class SegmentBase {
* @param timescale The timescale in units per second. * @param timescale The timescale in units per second.
* @param presentationTimeOffset The presentation time offset. The value in seconds is the * @param presentationTimeOffset The presentation time offset. The value in seconds is the
* division of this value and {@code timescale}. * 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 startNumber The sequence number of the first segment.
* @param duration The duration of each segment in the case of fixed duration segments. The * @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 * 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. * @param mediaSegments A list of {@link RangedUri}s indicating the locations of the segments.
*/ */
public SegmentList(RangedUri initialization, long timescale, long presentationTimeOffset, public SegmentList(RangedUri initialization, long timescale, long presentationTimeOffset,
long periodDurationMs, int startNumber, long duration, int startNumber, long duration, List<SegmentTimelineElement> segmentTimeline,
List<SegmentTimelineElement> segmentTimeline, List<RangedUri> mediaSegments) { List<RangedUri> mediaSegments) {
super(initialization, timescale, presentationTimeOffset, periodDurationMs, startNumber, super(initialization, timescale, presentationTimeOffset, startNumber, duration,
duration, segmentTimeline); segmentTimeline);
this.mediaSegments = mediaSegments; this.mediaSegments = mediaSegments;
} }
@ -268,7 +263,7 @@ public abstract class SegmentBase {
} }
@Override @Override
public int getLastSegmentNum() { public int getLastSegmentNum(long periodDurationUs) {
return startNumber + mediaSegments.size() - 1; return startNumber + mediaSegments.size() - 1;
} }
@ -296,7 +291,6 @@ public abstract class SegmentBase {
* @param timescale The timescale in units per second. * @param timescale The timescale in units per second.
* @param presentationTimeOffset The presentation time offset. The value in seconds is the * @param presentationTimeOffset The presentation time offset. The value in seconds is the
* division of this value and {@code timescale}. * 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 startNumber The sequence number of the first segment.
* @param duration The duration of each segment in the case of fixed duration segments. The * @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 * 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. * @param baseUrl A url to use as the base for relative urls generated by the templates.
*/ */
public SegmentTemplate(RangedUri initialization, long timescale, long presentationTimeOffset, public SegmentTemplate(RangedUri initialization, long timescale, long presentationTimeOffset,
long periodDurationMs, int startNumber, long duration, int startNumber, long duration, List<SegmentTimelineElement> segmentTimeline,
List<SegmentTimelineElement> segmentTimeline, UrlTemplate initializationTemplate, UrlTemplate initializationTemplate, UrlTemplate mediaTemplate, String baseUrl) {
UrlTemplate mediaTemplate, String baseUrl) { super(initialization, timescale, presentationTimeOffset, startNumber,
super(initialization, timescale, presentationTimeOffset, periodDurationMs, startNumber,
duration, segmentTimeline); duration, segmentTimeline);
this.initializationTemplate = initializationTemplate; this.initializationTemplate = initializationTemplate;
this.mediaTemplate = mediaTemplate; this.mediaTemplate = mediaTemplate;
@ -346,14 +339,14 @@ public abstract class SegmentBase {
} }
@Override @Override
public int getLastSegmentNum() { public int getLastSegmentNum(long periodDurationUs) {
if (segmentTimeline != null) { if (segmentTimeline != null) {
return segmentTimeline.size() + startNumber - 1; return segmentTimeline.size() + startNumber - 1;
} else if (periodDurationMs == -1) { } else if (periodDurationUs == C.UNKNOWN_TIME_US) {
return DashSegmentIndex.INDEX_UNBOUNDED; return DashSegmentIndex.INDEX_UNBOUNDED;
} else { } else {
long durationMs = (duration * 1000) / timescale; long durationUs = (duration * C.MICROS_PER_SECOND) / timescale;
return startNumber + (int) Util.ceilDivide(periodDurationMs, durationMs) - 1; return startNumber + (int) Util.ceilDivide(periodDurationUs, durationUs) - 1;
} }
} }