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:
parent
cb85dc25aa
commit
2f4c96781d
@ -188,7 +188,7 @@ public class DashRendererBuilder implements RendererBuilder {
|
||||
}
|
||||
|
||||
private void buildRenderers() {
|
||||
Period period = manifest.periods.get(0);
|
||||
Period period = manifest.getPeriod(0);
|
||||
Handler mainHandler = player.getMainHandler();
|
||||
LoadControl loadControl = new DefaultLoadControl(new DefaultAllocator(BUFFER_SEGMENT_SIZE));
|
||||
DefaultBandwidthMeter bandwidthMeter = new DefaultBandwidthMeter(mainHandler, player);
|
||||
|
@ -24,7 +24,6 @@ import com.google.android.exoplayer.chunk.ChunkSampleSource;
|
||||
import com.google.android.exoplayer.chunk.ChunkSource;
|
||||
import com.google.android.exoplayer.chunk.FormatEvaluator;
|
||||
import com.google.android.exoplayer.chunk.FormatEvaluator.AdaptiveEvaluator;
|
||||
import com.google.android.exoplayer.chunk.MultiTrackChunkSource;
|
||||
import com.google.android.exoplayer.dash.DashChunkSource;
|
||||
import com.google.android.exoplayer.dash.mpd.AdaptationSet;
|
||||
import com.google.android.exoplayer.dash.mpd.MediaPresentationDescription;
|
||||
@ -83,16 +82,16 @@ public class DashRendererBuilder implements ManifestCallback<MediaPresentationDe
|
||||
DefaultBandwidthMeter bandwidthMeter = new DefaultBandwidthMeter(null, null);
|
||||
|
||||
// Obtain Representations for playback.
|
||||
ArrayList<Representation> audioRepresentationsList = new ArrayList<>();
|
||||
Representation audioRepresentation = null;
|
||||
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++) {
|
||||
AdaptationSet adaptationSet = period.adaptationSets.get(i);
|
||||
int adaptationSetType = adaptationSet.type;
|
||||
for (int j = 0; j < adaptationSet.representations.size(); j++) {
|
||||
Representation representation = adaptationSet.representations.get(j);
|
||||
if (adaptationSetType == AdaptationSet.TYPE_AUDIO) {
|
||||
audioRepresentationsList.add(representation);
|
||||
if (adaptationSetType == AdaptationSet.TYPE_AUDIO && audioRepresentation == null) {
|
||||
audioRepresentation = representation;
|
||||
} else if (adaptationSetType == AdaptationSet.TYPE_VIDEO) {
|
||||
videoRepresentationsList.add(representation);
|
||||
}
|
||||
@ -109,7 +108,8 @@ public class DashRendererBuilder implements ManifestCallback<MediaPresentationDe
|
||||
String mimeType = videoRepresentations[0].format.mimeType;
|
||||
if (mimeType.equals(MimeTypes.VIDEO_WEBM)) {
|
||||
videoChunkSource = new DashChunkSource(videoDataSource,
|
||||
new AdaptiveEvaluator(bandwidthMeter), videoRepresentations);
|
||||
new AdaptiveEvaluator(bandwidthMeter), manifest.getPeriodDuration(0),
|
||||
videoRepresentations);
|
||||
} else {
|
||||
throw new IllegalStateException("Unexpected mime type: " + mimeType);
|
||||
}
|
||||
@ -120,21 +120,17 @@ public class DashRendererBuilder implements ManifestCallback<MediaPresentationDe
|
||||
}
|
||||
|
||||
// Build the audio renderer.
|
||||
MultiTrackChunkSource audioChunkSource = null;
|
||||
TrackRenderer audioRenderer = null;
|
||||
if (!audioRepresentationsList.isEmpty()) {
|
||||
TrackRenderer audioRenderer;
|
||||
if (audioRepresentation == null) {
|
||||
audioRenderer = null;
|
||||
} else {
|
||||
DataSource audioDataSource = new DefaultUriDataSource(player, bandwidthMeter, userAgent);
|
||||
ChunkSource[] audioChunkSources = new ChunkSource[audioRepresentationsList.size()];
|
||||
FormatEvaluator audioEvaluator = new FormatEvaluator.FixedEvaluator();
|
||||
for (int i = 0; i < audioRepresentationsList.size(); i++) {
|
||||
Representation representation = audioRepresentationsList.get(i);
|
||||
audioChunkSources[i] = new DashChunkSource(audioDataSource,
|
||||
audioEvaluator, representation);
|
||||
}
|
||||
audioChunkSource = new MultiTrackChunkSource(audioChunkSources);
|
||||
DashChunkSource audioChunkSource = new DashChunkSource(audioDataSource, audioEvaluator,
|
||||
manifest.getPeriodDuration(0), audioRepresentation);
|
||||
SampleSource audioSampleSource = new ChunkSampleSource(audioChunkSource, loadControl,
|
||||
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);
|
||||
} else {
|
||||
audioRenderer = new MediaCodecAudioTrackRenderer(audioSampleSource);
|
||||
|
@ -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_COUNT = 160;
|
||||
|
||||
|
||||
private boolean isDash;
|
||||
private String manifestUrl;
|
||||
private boolean useOpenGL;
|
||||
|
@ -19,7 +19,6 @@ import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
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.Format;
|
||||
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.util.FakeClock;
|
||||
import com.google.android.exoplayer.util.ManifestFetcher;
|
||||
import com.google.android.exoplayer.util.Util;
|
||||
|
||||
import android.test.InstrumentationTestCase;
|
||||
|
||||
import org.mockito.Mock;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
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 int MULTI_PERIOD_COUNT = 2;
|
||||
|
||||
private static final long MULTI_PERIOD_VOD_DURATION_MS = VOD_DURATION_MS * MULTI_PERIOD_COUNT;
|
||||
private static final long MULTI_PERIOD_LIVE_DURATION_MS = LIVE_DURATION_MS * MULTI_PERIOD_COUNT;
|
||||
|
||||
private static final long AVAILABILITY_START_TIME_MS = 60000;
|
||||
private static final long AVAILABILITY_REALTIME_OFFSET_MS = 1000;
|
||||
private static final long AVAILABILITY_CURRENT_TIME_MS =
|
||||
AVAILABILITY_START_TIME_MS + LIVE_TIMESHIFT_BUFFER_DEPTH_MS - AVAILABILITY_REALTIME_OFFSET_MS;
|
||||
private static final long ELAPSED_REALTIME_OFFSET_MS = 1000;
|
||||
|
||||
private static final int TALL_HEIGHT = 200;
|
||||
private static final int WIDE_WIDTH = 400;
|
||||
@ -84,16 +79,13 @@ public class DashChunkSourceTest extends InstrumentationTestCase {
|
||||
private static final Format WIDE_VIDEO =
|
||||
new Format("3", "video/mp4", WIDE_WIDTH, 50, -1, -1, -1, 1000);
|
||||
|
||||
@Mock
|
||||
private DataSource mockDataSource;
|
||||
|
||||
@Override
|
||||
public void setUp() throws Exception {
|
||||
TestUtil.setUpMockito(this);
|
||||
}
|
||||
|
||||
public void testGetAvailableRangeOnVod() {
|
||||
DashChunkSource chunkSource = new DashChunkSource(generateVodMpd(), AdaptationSet.TYPE_VIDEO,
|
||||
DashChunkSource chunkSource = new DashChunkSource(buildVodMpd(), AdaptationSet.TYPE_VIDEO,
|
||||
null, null, mock(FormatEvaluator.class));
|
||||
chunkSource.enable(0);
|
||||
TimeRange availableRange = chunkSource.getAvailableRange();
|
||||
@ -105,54 +97,31 @@ public class DashChunkSourceTest extends InstrumentationTestCase {
|
||||
assertEquals(VOD_DURATION_MS, seekRangeValuesMs[1]);
|
||||
}
|
||||
|
||||
public void testGetAvailableRangeOnLiveWithTimelineNoEdgeLatency() {
|
||||
long liveEdgeLatency = 0;
|
||||
MediaPresentationDescription mpd = generateLiveMpdWithTimeline(0, 0, LIVE_DURATION_MS);
|
||||
DashChunkSource chunkSource = setupDashChunkSource(mpd, 0, liveEdgeLatency);
|
||||
public void testGetAvailableRangeOnLiveWithTimeline() {
|
||||
MediaPresentationDescription mpd = buildLiveMpdWithTimeline(LIVE_DURATION_MS, 0);
|
||||
DashChunkSource chunkSource = buildDashChunkSource(mpd);
|
||||
TimeRange availableRange = chunkSource.getAvailableRange();
|
||||
|
||||
checkAvailableRange(availableRange, 0, LIVE_DURATION_MS * 1000);
|
||||
}
|
||||
|
||||
public void testGetAvailableRangeOnLiveWithTimeline500msEdgeLatency() {
|
||||
long liveEdgeLatency = 500;
|
||||
MediaPresentationDescription mpd = generateLiveMpdWithTimeline(0, 0, LIVE_DURATION_MS);
|
||||
DashChunkSource chunkSource = setupDashChunkSource(mpd, 0, liveEdgeLatency);
|
||||
TimeRange availableRange = chunkSource.getAvailableRange();
|
||||
|
||||
checkAvailableRange(availableRange, 0, LIVE_DURATION_MS * 1000);
|
||||
}
|
||||
|
||||
public void testGetAvailableRangeOnMultiPeriodVod() {
|
||||
DashChunkSource chunkSource = new DashChunkSource(generateMultiPeriodVodMpd(),
|
||||
DashChunkSource chunkSource = new DashChunkSource(buildMultiPeriodVodMpd(),
|
||||
AdaptationSet.TYPE_VIDEO, null, null, EVALUATOR);
|
||||
chunkSource.enable(0);
|
||||
TimeRange availableRange = chunkSource.getAvailableRange();
|
||||
|
||||
checkAvailableRange(availableRange, 0, MULTI_PERIOD_VOD_DURATION_MS * 1000);
|
||||
}
|
||||
|
||||
public void testGetSeekRangeOnMultiPeriodLiveWithTimelineNoEdgeLatency() {
|
||||
long liveEdgeLatency = 0;
|
||||
MediaPresentationDescription mpd = generateMultiPeriodLiveMpdWithTimeline(0);
|
||||
DashChunkSource chunkSource = setupDashChunkSource(mpd, 0, liveEdgeLatency);
|
||||
public void testGetSeekRangeOnMultiPeriodLiveWithTimeline() {
|
||||
MediaPresentationDescription mpd = buildMultiPeriodLiveMpdWithTimeline();
|
||||
DashChunkSource chunkSource = buildDashChunkSource(mpd);
|
||||
TimeRange availableRange = chunkSource.getAvailableRange();
|
||||
|
||||
checkAvailableRange(availableRange, 0, MULTI_PERIOD_LIVE_DURATION_MS * 1000);
|
||||
}
|
||||
|
||||
public void testGetSeekRangeOnMultiPeriodLiveWithTimeline500msEdgeLatency() {
|
||||
long liveEdgeLatency = 500;
|
||||
MediaPresentationDescription mpd = generateMultiPeriodLiveMpdWithTimeline(0);
|
||||
DashChunkSource chunkSource = setupDashChunkSource(mpd, 0, liveEdgeLatency);
|
||||
TimeRange availableRange = chunkSource.getAvailableRange();
|
||||
|
||||
checkAvailableRange(availableRange, 0, MULTI_PERIOD_LIVE_DURATION_MS * 1000);
|
||||
}
|
||||
|
||||
public void testSegmentIndexInitializationOnVod() {
|
||||
DashChunkSource chunkSource = new DashChunkSource(generateVodMpd(),
|
||||
AdaptationSet.TYPE_VIDEO, null, mockDataSource, EVALUATOR);
|
||||
DashChunkSource chunkSource = new DashChunkSource(buildVodMpd(), AdaptationSet.TYPE_VIDEO,
|
||||
null, mock(DataSource.class), EVALUATOR);
|
||||
chunkSource.enable(0);
|
||||
|
||||
List<MediaChunk> queue = new ArrayList<>();
|
||||
@ -166,394 +135,258 @@ public class DashChunkSourceTest extends InstrumentationTestCase {
|
||||
}
|
||||
|
||||
public void testSegmentRequestSequenceOnMultiPeriodLiveWithTimeline() {
|
||||
long liveEdgeLatency = 0;
|
||||
MediaPresentationDescription mpd = generateMultiPeriodLiveMpdWithTimeline(0);
|
||||
DashChunkSource chunkSource = setupDashChunkSource(mpd, 0, liveEdgeLatency);
|
||||
|
||||
MediaPresentationDescription mpd = buildMultiPeriodLiveMpdWithTimeline();
|
||||
DashChunkSource chunkSource = buildDashChunkSource(mpd);
|
||||
checkSegmentRequestSequenceOnMultiPeriodLive(chunkSource);
|
||||
}
|
||||
|
||||
public void testSegmentRequestSequenceOnMultiPeriodLiveWithTemplate() {
|
||||
long liveEdgeLatency = 0;
|
||||
MediaPresentationDescription mpd = generateMultiPeriodLiveMpdWithTemplate(0);
|
||||
DashChunkSource chunkSource = setupDashChunkSource(mpd, 0, liveEdgeLatency,
|
||||
AVAILABILITY_CURRENT_TIME_MS + LIVE_DURATION_MS);
|
||||
|
||||
MediaPresentationDescription mpd = buildMultiPeriodLiveMpdWithTemplate();
|
||||
DashChunkSource chunkSource = buildDashChunkSource(mpd);
|
||||
checkSegmentRequestSequenceOnMultiPeriodLive(chunkSource);
|
||||
}
|
||||
|
||||
|
||||
public void testLiveEdgeNoLatency() {
|
||||
long startTimeMs = 0;
|
||||
long liveEdgeLatencyMs = 0;
|
||||
long seekPositionMs = startTimeMs + LIVE_DURATION_MS - liveEdgeLatencyMs;
|
||||
public void testLiveEdgeLatency() {
|
||||
long availableRangeStartMs = 0;
|
||||
long availableRangeEndMs = LIVE_DURATION_MS;
|
||||
long seekPositionMs = LIVE_DURATION_MS;
|
||||
|
||||
long chunkStartTimeMs = 4000;
|
||||
long chunkEndTimeMs = 5000;
|
||||
// Test with 1-1000ms latency.
|
||||
long liveEdgeLatency = 1;
|
||||
checkLiveEdgeConsistency(LIVE_DURATION_MS, 0, liveEdgeLatency, seekPositionMs,
|
||||
availableRangeStartMs, availableRangeEndMs, chunkStartTimeMs, chunkEndTimeMs);
|
||||
liveEdgeLatency = 1000;
|
||||
checkLiveEdgeConsistency(LIVE_DURATION_MS, 0, liveEdgeLatency, seekPositionMs,
|
||||
availableRangeStartMs, availableRangeEndMs, chunkStartTimeMs, chunkEndTimeMs);
|
||||
|
||||
checkLiveTimelineConsistency(startTimeMs, liveEdgeLatencyMs, seekPositionMs,
|
||||
chunkStartTimeMs = 3000;
|
||||
chunkEndTimeMs = 4000;
|
||||
// Test with 1001-2000ms latency.
|
||||
liveEdgeLatency = 1001;
|
||||
checkLiveEdgeConsistency(LIVE_DURATION_MS, 0, liveEdgeLatency, seekPositionMs,
|
||||
availableRangeStartMs, availableRangeEndMs, chunkStartTimeMs, chunkEndTimeMs);
|
||||
liveEdgeLatency = 2000;
|
||||
checkLiveEdgeConsistency(LIVE_DURATION_MS, 0, liveEdgeLatency, seekPositionMs,
|
||||
availableRangeStartMs, availableRangeEndMs, chunkStartTimeMs, chunkEndTimeMs);
|
||||
|
||||
chunkStartTimeMs = 0;
|
||||
chunkEndTimeMs = 1000;
|
||||
// Test with 9001-10000 latency.
|
||||
liveEdgeLatency = 9001;
|
||||
checkLiveEdgeConsistency(LIVE_DURATION_MS, 0, liveEdgeLatency, seekPositionMs,
|
||||
availableRangeStartMs, availableRangeEndMs, chunkStartTimeMs, chunkEndTimeMs);
|
||||
liveEdgeLatency = 10000;
|
||||
checkLiveEdgeConsistency(LIVE_DURATION_MS, 0, liveEdgeLatency, seekPositionMs,
|
||||
availableRangeStartMs, availableRangeEndMs, chunkStartTimeMs, chunkEndTimeMs);
|
||||
|
||||
// Test with 10001 latency. Seek position will be bounded to the first chunk.
|
||||
liveEdgeLatency = 10001;
|
||||
checkLiveEdgeConsistency(LIVE_DURATION_MS, 0, liveEdgeLatency, seekPositionMs,
|
||||
availableRangeStartMs, availableRangeEndMs, chunkStartTimeMs, chunkEndTimeMs);
|
||||
}
|
||||
|
||||
public void testLiveEdgeAlmostNoLatency() {
|
||||
long startTimeMs = 0;
|
||||
long liveEdgeLatencyMs = 1;
|
||||
long seekPositionMs = startTimeMs + LIVE_DURATION_MS - liveEdgeLatencyMs;
|
||||
long availableRangeStartMs = 0;
|
||||
long availableRangeEndMs = LIVE_DURATION_MS;
|
||||
long chunkStartTimeMs = 4000;
|
||||
long chunkEndTimeMs = 5000;
|
||||
// Private methods.
|
||||
|
||||
checkLiveTimelineConsistency(startTimeMs, liveEdgeLatencyMs, seekPositionMs,
|
||||
availableRangeStartMs, availableRangeEndMs, chunkStartTimeMs, chunkEndTimeMs);
|
||||
}
|
||||
|
||||
public void testLiveEdge500msLatency() {
|
||||
long startTimeMs = 0;
|
||||
long liveEdgeLatencyMs = 500;
|
||||
long seekPositionMs = startTimeMs + LIVE_DURATION_MS - liveEdgeLatencyMs;
|
||||
long availableRangeStartMs = 0;
|
||||
long availableRangeEndMs = LIVE_DURATION_MS;
|
||||
long chunkStartTimeMs = 4000;
|
||||
long chunkEndTimeMs = 5000;
|
||||
|
||||
checkLiveTimelineConsistency(startTimeMs, liveEdgeLatencyMs, seekPositionMs,
|
||||
availableRangeStartMs, availableRangeEndMs, chunkStartTimeMs, chunkEndTimeMs);
|
||||
}
|
||||
|
||||
public void testLiveEdge1000msLatency() {
|
||||
long startTimeMs = 0;
|
||||
long liveEdgeLatencyMs = 1000;
|
||||
long seekPositionMs = startTimeMs + LIVE_DURATION_MS - liveEdgeLatencyMs;
|
||||
long availableRangeStartMs = 0;
|
||||
long availableRangeEndMs = LIVE_DURATION_MS;
|
||||
long chunkStartTimeMs = 4000;
|
||||
long chunkEndTimeMs = 5000;
|
||||
|
||||
checkLiveTimelineConsistency(startTimeMs, liveEdgeLatencyMs, seekPositionMs,
|
||||
availableRangeStartMs, availableRangeEndMs, chunkStartTimeMs, chunkEndTimeMs);
|
||||
}
|
||||
|
||||
public void testLiveEdge1001msLatency() {
|
||||
long startTimeMs = 0;
|
||||
long liveEdgeLatencyMs = 1001;
|
||||
long seekPositionMs = startTimeMs + LIVE_DURATION_MS - liveEdgeLatencyMs;
|
||||
long availableRangeStartMs = 0;
|
||||
long availableRangeEndMs = LIVE_DURATION_MS;
|
||||
long chunkStartTimeMs = 3000;
|
||||
long chunkEndTimeMs = 4000;
|
||||
|
||||
checkLiveTimelineConsistency(startTimeMs, liveEdgeLatencyMs, seekPositionMs,
|
||||
availableRangeStartMs, availableRangeEndMs, chunkStartTimeMs, chunkEndTimeMs);
|
||||
}
|
||||
|
||||
public void testLiveEdge2500msLatency() {
|
||||
long startTimeMs = 0;
|
||||
long liveEdgeLatencyMs = 2500;
|
||||
long seekPositionMs = startTimeMs + LIVE_DURATION_MS - liveEdgeLatencyMs;
|
||||
long availableRangeStartMs = 0;
|
||||
long availableRangeEndMs = LIVE_DURATION_MS;
|
||||
long chunkStartTimeMs = 2000;
|
||||
long chunkEndTimeMs = 3000;
|
||||
|
||||
checkLiveTimelineConsistency(startTimeMs, liveEdgeLatencyMs, seekPositionMs,
|
||||
availableRangeStartMs, availableRangeEndMs, chunkStartTimeMs, chunkEndTimeMs);
|
||||
}
|
||||
|
||||
public void testLiveEdgeVeryHighLatency() {
|
||||
long startTimeMs = 0;
|
||||
long liveEdgeLatencyMs = 10000;
|
||||
long seekPositionMs = startTimeMs + LIVE_DURATION_MS - liveEdgeLatencyMs;
|
||||
long availableRangeStartMs = 0;
|
||||
long availableRangeEndMs = LIVE_DURATION_MS;
|
||||
long chunkStartTimeMs = 0;
|
||||
long chunkEndTimeMs = 1000;
|
||||
|
||||
checkLiveTimelineConsistency(startTimeMs, liveEdgeLatencyMs, seekPositionMs,
|
||||
availableRangeStartMs, availableRangeEndMs, chunkStartTimeMs, chunkEndTimeMs);
|
||||
}
|
||||
|
||||
public void testLiveEdgeNoLatencyInProgress() {
|
||||
long startTimeMs = 3000;
|
||||
long liveEdgeLatencyMs = 0;
|
||||
long seekPositionMs = startTimeMs + LIVE_DURATION_MS - liveEdgeLatencyMs;
|
||||
long availableRangeStartMs = 3000;
|
||||
long availableRangeEndMs = 3000 + LIVE_DURATION_MS;
|
||||
long chunkStartTimeMs = 7000;
|
||||
long chunkEndTimeMs = 8000;
|
||||
|
||||
checkLiveTimelineConsistency(startTimeMs, liveEdgeLatencyMs, seekPositionMs,
|
||||
availableRangeStartMs, availableRangeEndMs, chunkStartTimeMs, chunkEndTimeMs);
|
||||
}
|
||||
|
||||
public void testLiveEdgeAlmostNoLatencyInProgress() {
|
||||
long startTimeMs = 3000;
|
||||
long liveEdgeLatencyMs = 1;
|
||||
long seekPositionMs = startTimeMs + LIVE_DURATION_MS - liveEdgeLatencyMs;
|
||||
long availableRangeStartMs = 3000;
|
||||
long availableRangeEndMs = 3000 + LIVE_DURATION_MS;
|
||||
long chunkStartTimeMs = 7000;
|
||||
long chunkEndTimeMs = 8000;
|
||||
|
||||
checkLiveTimelineConsistency(startTimeMs, liveEdgeLatencyMs, seekPositionMs,
|
||||
availableRangeStartMs, availableRangeEndMs, chunkStartTimeMs, chunkEndTimeMs);
|
||||
}
|
||||
|
||||
public void testLiveEdge500msLatencyInProgress() {
|
||||
long startTimeMs = 3000;
|
||||
long liveEdgeLatencyMs = 500;
|
||||
long seekPositionMs = startTimeMs + LIVE_DURATION_MS - liveEdgeLatencyMs;
|
||||
long availableRangeStartMs = 3000;
|
||||
long availableRangeEndMs = 3000 + LIVE_DURATION_MS;
|
||||
long chunkStartTimeMs = 7000;
|
||||
long chunkEndTimeMs = 8000;
|
||||
|
||||
checkLiveTimelineConsistency(startTimeMs, liveEdgeLatencyMs, seekPositionMs,
|
||||
availableRangeStartMs, availableRangeEndMs, chunkStartTimeMs, chunkEndTimeMs);
|
||||
}
|
||||
|
||||
public void testLiveEdge1000msLatencyInProgress() {
|
||||
long startTimeMs = 3000;
|
||||
long liveEdgeLatencyMs = 1000;
|
||||
long seekPositionMs = startTimeMs + LIVE_DURATION_MS - liveEdgeLatencyMs;
|
||||
long availableRangeStartMs = 3000;
|
||||
long availableRangeEndMs = 3000 + LIVE_DURATION_MS;
|
||||
long chunkStartTimeMs = 7000;
|
||||
long chunkEndTimeMs = 8000;
|
||||
|
||||
checkLiveTimelineConsistency(startTimeMs, liveEdgeLatencyMs, seekPositionMs,
|
||||
availableRangeStartMs, availableRangeEndMs, chunkStartTimeMs, chunkEndTimeMs);
|
||||
}
|
||||
|
||||
public void testLiveEdge1001msLatencyInProgress() {
|
||||
long startTimeMs = 3000;
|
||||
long liveEdgeLatencyMs = 1001;
|
||||
long seekPositionMs = startTimeMs + LIVE_DURATION_MS - liveEdgeLatencyMs;
|
||||
long availableRangeStartMs = 3000;
|
||||
long availableRangeEndMs = 3000 + LIVE_DURATION_MS;
|
||||
long chunkStartTimeMs = 6000;
|
||||
long chunkEndTimeMs = 7000;
|
||||
|
||||
checkLiveTimelineConsistency(startTimeMs, liveEdgeLatencyMs, seekPositionMs,
|
||||
availableRangeStartMs, availableRangeEndMs, chunkStartTimeMs, chunkEndTimeMs);
|
||||
}
|
||||
|
||||
public void testLiveEdge2500msLatencyInProgress() {
|
||||
long startTimeMs = 3000;
|
||||
long liveEdgeLatencyMs = 2500;
|
||||
long seekPositionMs = startTimeMs + LIVE_DURATION_MS - liveEdgeLatencyMs;
|
||||
long availableRangeStartMs = 3000;
|
||||
long availableRangeEndMs = 3000 + LIVE_DURATION_MS;
|
||||
long chunkStartTimeMs = 5000;
|
||||
long chunkEndTimeMs = 6000;
|
||||
|
||||
checkLiveTimelineConsistency(startTimeMs, liveEdgeLatencyMs, seekPositionMs,
|
||||
availableRangeStartMs, availableRangeEndMs, chunkStartTimeMs, chunkEndTimeMs);
|
||||
}
|
||||
|
||||
public void testLiveEdgeVeryHighLatencyInProgress() {
|
||||
long startTimeMs = 3000;
|
||||
long liveEdgeLatencyMs = 10000;
|
||||
long seekPositionMs = startTimeMs + LIVE_DURATION_MS - liveEdgeLatencyMs;
|
||||
long availableRangeStartMs = 3000;
|
||||
long availableRangeEndMs = 3000 + LIVE_DURATION_MS;
|
||||
long chunkStartTimeMs = 3000;
|
||||
long chunkEndTimeMs = 4000;
|
||||
|
||||
checkLiveEdgeLatencyWithTimeline(startTimeMs, 0, liveEdgeLatencyMs, seekPositionMs,
|
||||
availableRangeStartMs, availableRangeEndMs, chunkStartTimeMs, chunkEndTimeMs);
|
||||
checkLiveEdgeLatencyWithTimeline(0, startTimeMs, liveEdgeLatencyMs, seekPositionMs,
|
||||
availableRangeStartMs, availableRangeEndMs, chunkStartTimeMs, chunkEndTimeMs);
|
||||
checkLiveEdgeLatencyWithTemplateAndUnlimitedTimeshift(startTimeMs, liveEdgeLatencyMs,
|
||||
0, availableRangeEndMs, 0, 1000);
|
||||
checkLiveEdgeLatencyWithTemplateAndLimitedTimeshift(startTimeMs, liveEdgeLatencyMs,
|
||||
seekPositionMs, availableRangeStartMs, availableRangeEndMs, chunkStartTimeMs,
|
||||
chunkEndTimeMs);
|
||||
}
|
||||
|
||||
private static Representation generateVodRepresentation(long startTimeMs, long duration,
|
||||
Format format) {
|
||||
private static Representation buildVodRepresentation(Format format) {
|
||||
RangedUri rangedUri = new RangedUri("https://example.com/1.mp4", null, 0, 100);
|
||||
SingleSegmentBase segmentBase = new SingleSegmentBase(rangedUri, 1, 0,
|
||||
"https://example.com/1.mp4", 0, -1);
|
||||
return Representation.newInstance(startTimeMs, duration, null, 0, format, segmentBase);
|
||||
return Representation.newInstance(null, 0, format, segmentBase);
|
||||
}
|
||||
|
||||
private static Representation generateSegmentTimelineRepresentation(long segmentStartMs,
|
||||
long periodStartMs, long duration) {
|
||||
private static Representation buildSegmentTimelineRepresentation(long timelineDurationMs,
|
||||
long timelineStartTimeMs) {
|
||||
List<SegmentTimelineElement> segmentTimeline = new ArrayList<>();
|
||||
List<RangedUri> mediaSegments = new ArrayList<>();
|
||||
long segmentStartTimeMs = segmentStartMs;
|
||||
long segmentStartTimeMs = timelineStartTimeMs;
|
||||
long byteStart = 0;
|
||||
for (int i = 0; i < (duration / LIVE_SEGMENT_DURATION_MS); i++) {
|
||||
// Create all but the last segment with LIVE_SEGMENT_DURATION_MS.
|
||||
int segmentCount = (int) Util.ceilDivide(timelineDurationMs, LIVE_SEGMENT_DURATION_MS);
|
||||
for (int i = 0; i < segmentCount - 1; i++) {
|
||||
segmentTimeline.add(new SegmentTimelineElement(segmentStartTimeMs, LIVE_SEGMENT_DURATION_MS));
|
||||
mediaSegments.add(new RangedUri("", "", byteStart, 500L));
|
||||
segmentStartTimeMs += LIVE_SEGMENT_DURATION_MS;
|
||||
byteStart += 500;
|
||||
}
|
||||
|
||||
int startNumber = (int) ((periodStartMs + segmentStartMs) / LIVE_SEGMENT_DURATION_MS);
|
||||
MultiSegmentBase segmentBase = new SegmentList(null, 1000, 0,
|
||||
TrackRenderer.UNKNOWN_TIME_US, startNumber, TrackRenderer.UNKNOWN_TIME_US, segmentTimeline,
|
||||
// The final segment duration is calculated so that the total duration is timelineDurationMs.
|
||||
long finalSegmentDurationMs = (timelineStartTimeMs + timelineDurationMs) - segmentStartTimeMs;
|
||||
segmentTimeline.add(new SegmentTimelineElement(segmentStartTimeMs, finalSegmentDurationMs));
|
||||
mediaSegments.add(new RangedUri("", "", byteStart, 500L));
|
||||
segmentStartTimeMs += finalSegmentDurationMs;
|
||||
byteStart += 500;
|
||||
// Construct the list.
|
||||
MultiSegmentBase segmentBase = new SegmentList(null, 1000, 0, 0, 0, segmentTimeline,
|
||||
mediaSegments);
|
||||
return Representation.newInstance(periodStartMs, TrackRenderer.UNKNOWN_TIME_US, null, 0,
|
||||
REGULAR_VIDEO, segmentBase);
|
||||
return Representation.newInstance(null, 0, REGULAR_VIDEO, segmentBase);
|
||||
}
|
||||
|
||||
private static Representation generateSegmentTemplateRepresentation(long periodStartMs,
|
||||
long periodDurationMs) {
|
||||
private static Representation buildSegmentTemplateRepresentation() {
|
||||
UrlTemplate initializationTemplate = null;
|
||||
UrlTemplate mediaTemplate = UrlTemplate.compile("$RepresentationID$/$Number$");
|
||||
int startNumber = (int) (periodStartMs / LIVE_SEGMENT_DURATION_MS);
|
||||
MultiSegmentBase segmentBase = new SegmentTemplate(null, 1000, 0,
|
||||
periodDurationMs, startNumber, LIVE_SEGMENT_DURATION_MS, null,
|
||||
initializationTemplate, mediaTemplate, "http://www.youtube.com");
|
||||
return Representation.newInstance(periodStartMs, periodDurationMs, null, 0, REGULAR_VIDEO,
|
||||
segmentBase);
|
||||
MultiSegmentBase segmentBase = new SegmentTemplate(null, 1000, 0, 0, LIVE_SEGMENT_DURATION_MS,
|
||||
null, initializationTemplate, mediaTemplate, "http://www.youtube.com");
|
||||
return Representation.newInstance(null, 0, REGULAR_VIDEO, segmentBase);
|
||||
}
|
||||
|
||||
private static MediaPresentationDescription generateMpd(boolean live,
|
||||
List<Representation> representations, boolean limitTimeshiftBuffer) {
|
||||
Representation firstRepresentation = representations.get(0);
|
||||
private static MediaPresentationDescription buildMpd(long durationMs,
|
||||
List<Representation> representations, boolean live, boolean limitTimeshiftBuffer) {
|
||||
AdaptationSet adaptationSet = new AdaptationSet(0, AdaptationSet.TYPE_UNKNOWN, representations);
|
||||
Period period = new Period(null, firstRepresentation.periodStartMs,
|
||||
firstRepresentation.periodDurationMs, Collections.singletonList(adaptationSet));
|
||||
long duration = (live) ? TrackRenderer.UNKNOWN_TIME_US
|
||||
: firstRepresentation.periodDurationMs - firstRepresentation.periodStartMs;
|
||||
return new MediaPresentationDescription(AVAILABILITY_START_TIME_MS, duration, -1, live, -1,
|
||||
Period period = new Period(null, 0, Collections.singletonList(adaptationSet));
|
||||
return new MediaPresentationDescription(AVAILABILITY_START_TIME_MS, durationMs, -1, live, -1,
|
||||
(limitTimeshiftBuffer) ? LIVE_TIMESHIFT_BUFFER_DEPTH_MS : -1, null, null,
|
||||
Collections.singletonList(period));
|
||||
}
|
||||
|
||||
private static MediaPresentationDescription generateMultiPeriodMpd(boolean live,
|
||||
List<Period> periods, boolean limitTimeshiftBuffer) {
|
||||
Period firstPeriod = periods.get(0);
|
||||
Period lastPeriod = periods.get(periods.size() - 1);
|
||||
long duration = (live) ? TrackRenderer.UNKNOWN_TIME_US
|
||||
: (lastPeriod.startMs + lastPeriod.durationMs - firstPeriod.startMs);
|
||||
return new MediaPresentationDescription(AVAILABILITY_START_TIME_MS, duration, -1, live, -1,
|
||||
private static MediaPresentationDescription buildMultiPeriodMpd(long durationMs,
|
||||
List<Period> periods, boolean live, boolean limitTimeshiftBuffer) {
|
||||
return new MediaPresentationDescription(AVAILABILITY_START_TIME_MS, durationMs, -1, live, -1,
|
||||
(limitTimeshiftBuffer) ? LIVE_TIMESHIFT_BUFFER_DEPTH_MS : -1,
|
||||
null, null, periods);
|
||||
}
|
||||
|
||||
private static MediaPresentationDescription generateVodMpd() {
|
||||
private static MediaPresentationDescription buildVodMpd() {
|
||||
List<Representation> representations = new ArrayList<>();
|
||||
|
||||
representations.add(generateVodRepresentation(0, VOD_DURATION_MS, TALL_VIDEO));
|
||||
representations.add(generateVodRepresentation(0, VOD_DURATION_MS, WIDE_VIDEO));
|
||||
|
||||
return generateMpd(false, representations, false);
|
||||
representations.add(buildVodRepresentation(TALL_VIDEO));
|
||||
representations.add(buildVodRepresentation(WIDE_VIDEO));
|
||||
return buildMpd(VOD_DURATION_MS, representations, false, false);
|
||||
}
|
||||
|
||||
private MediaPresentationDescription generateMultiPeriodVodMpd() {
|
||||
private static MediaPresentationDescription buildMultiPeriodVodMpd() {
|
||||
List<Period> periods = new ArrayList<>();
|
||||
long startTimeMs = 0;
|
||||
|
||||
long duration = VOD_DURATION_MS;
|
||||
long timeMs = 0;
|
||||
long periodDurationMs = VOD_DURATION_MS;
|
||||
for (int i = 0; i < 2; i++) {
|
||||
Representation representation = generateVodRepresentation(startTimeMs, duration,
|
||||
REGULAR_VIDEO);
|
||||
Representation representation = buildVodRepresentation(REGULAR_VIDEO);
|
||||
AdaptationSet adaptationSet = new AdaptationSet(0, AdaptationSet.TYPE_UNKNOWN,
|
||||
Collections.singletonList(representation));
|
||||
Period period = new Period(null, startTimeMs, duration,
|
||||
Collections.singletonList(adaptationSet));
|
||||
Period period = new Period(null, timeMs, Collections.singletonList(adaptationSet));
|
||||
periods.add(period);
|
||||
startTimeMs += duration;
|
||||
timeMs += periodDurationMs;
|
||||
}
|
||||
|
||||
return generateMultiPeriodMpd(false, periods, false);
|
||||
return buildMultiPeriodMpd(timeMs, periods, false, false);
|
||||
}
|
||||
|
||||
private static MediaPresentationDescription generateLiveMpdWithTimeline(long segmentStartMs,
|
||||
long periodStartMs, long durationMs) {
|
||||
return generateMpd(true, Collections.singletonList(generateSegmentTimelineRepresentation(
|
||||
segmentStartMs, periodStartMs, durationMs)), false);
|
||||
private static MediaPresentationDescription buildLiveMpdWithTimeline(long durationMs,
|
||||
long timelineStartTimeMs) {
|
||||
Representation representation = buildSegmentTimelineRepresentation(
|
||||
durationMs - timelineStartTimeMs, timelineStartTimeMs);
|
||||
return buildMpd(durationMs, Collections.singletonList(representation), true, false);
|
||||
}
|
||||
|
||||
private static MediaPresentationDescription generateLiveMpdWithTemplate(long periodStartMs,
|
||||
long periodDurationMs, boolean limitTimeshiftBuffer) {
|
||||
return generateMpd(true, Collections.singletonList(generateSegmentTemplateRepresentation(
|
||||
periodStartMs, periodDurationMs)), limitTimeshiftBuffer);
|
||||
private static MediaPresentationDescription buildLiveMpdWithTemplate(long durationMs,
|
||||
boolean limitTimeshiftBuffer) {
|
||||
Representation representation = buildSegmentTemplateRepresentation();
|
||||
return buildMpd(durationMs, Collections.singletonList(representation), true,
|
||||
limitTimeshiftBuffer);
|
||||
}
|
||||
|
||||
private static MediaPresentationDescription generateMultiPeriodLiveMpdWithTimeline(
|
||||
long startTimeMs) {
|
||||
private static MediaPresentationDescription buildMultiPeriodLiveMpdWithTimeline() {
|
||||
List<Period> periods = new ArrayList<>();
|
||||
|
||||
long periodStartTimeMs = 0;
|
||||
long periodDurationMs = LIVE_DURATION_MS;
|
||||
for (int i = 0; i < MULTI_PERIOD_COUNT; i++) {
|
||||
Representation representation = generateSegmentTimelineRepresentation(0, startTimeMs,
|
||||
LIVE_DURATION_MS);
|
||||
Representation representation = buildSegmentTimelineRepresentation(LIVE_DURATION_MS, 0);
|
||||
AdaptationSet adaptationSet = new AdaptationSet(0, AdaptationSet.TYPE_UNKNOWN,
|
||||
Collections.singletonList(representation));
|
||||
long duration = (i < MULTI_PERIOD_COUNT - 1) ? MULTI_PERIOD_COUNT
|
||||
: TrackRenderer.END_OF_TRACK_US;
|
||||
Period period = new Period(null, startTimeMs, duration,
|
||||
Collections.singletonList(adaptationSet));
|
||||
Period period = new Period(null, periodStartTimeMs, Collections.singletonList(adaptationSet));
|
||||
periods.add(period);
|
||||
startTimeMs += LIVE_DURATION_MS;
|
||||
periodStartTimeMs += periodDurationMs;
|
||||
}
|
||||
|
||||
return generateMultiPeriodMpd(true, periods, false);
|
||||
return buildMultiPeriodMpd(periodDurationMs, periods, true, false);
|
||||
}
|
||||
|
||||
private static MediaPresentationDescription generateMultiPeriodLiveMpdWithTemplate(
|
||||
long periodStartTimeMs) {
|
||||
private static MediaPresentationDescription buildMultiPeriodLiveMpdWithTemplate() {
|
||||
List<Period> periods = new ArrayList<>();
|
||||
|
||||
Representation representation1 = generateSegmentTemplateRepresentation(periodStartTimeMs,
|
||||
LIVE_DURATION_MS);
|
||||
AdaptationSet adaptationSet1 = new AdaptationSet(0, AdaptationSet.TYPE_UNKNOWN,
|
||||
Collections.singletonList(representation1));
|
||||
Period period1 = new Period(null, periodStartTimeMs, LIVE_DURATION_MS,
|
||||
Collections.singletonList(adaptationSet1));
|
||||
periods.add(period1);
|
||||
|
||||
periodStartTimeMs += LIVE_DURATION_MS;
|
||||
|
||||
Representation representation2 = generateSegmentTemplateRepresentation(periodStartTimeMs,
|
||||
TrackRenderer.UNKNOWN_TIME_US);
|
||||
AdaptationSet adaptationSet2 = new AdaptationSet(0, AdaptationSet.TYPE_UNKNOWN,
|
||||
Collections.singletonList(representation2));
|
||||
Period period2 = new Period(null, periodStartTimeMs, TrackRenderer.UNKNOWN_TIME_US,
|
||||
Collections.singletonList(adaptationSet2));
|
||||
periods.add(period2);
|
||||
|
||||
return generateMultiPeriodMpd(true, periods, false);
|
||||
long periodStartTimeMs = 0;
|
||||
long periodDurationMs = LIVE_DURATION_MS;
|
||||
for (int i = 0; i < MULTI_PERIOD_COUNT; i++) {
|
||||
Representation representation = buildSegmentTemplateRepresentation();
|
||||
AdaptationSet adaptationSet = new AdaptationSet(0, AdaptationSet.TYPE_UNKNOWN,
|
||||
Collections.singletonList(representation));
|
||||
Period period = new Period(null, periodStartTimeMs, Collections.singletonList(adaptationSet));
|
||||
periods.add(period);
|
||||
periodStartTimeMs += periodDurationMs;
|
||||
}
|
||||
return buildMultiPeriodMpd(MULTI_PERIOD_LIVE_DURATION_MS, periods, true, false);
|
||||
}
|
||||
|
||||
private DashChunkSource setupDashChunkSource(MediaPresentationDescription mpd, long periodStartMs,
|
||||
long liveEdgeLatencyMs) {
|
||||
return setupDashChunkSource(mpd, periodStartMs, liveEdgeLatencyMs,
|
||||
AVAILABILITY_CURRENT_TIME_MS + periodStartMs);
|
||||
private static DashChunkSource buildDashChunkSource(MediaPresentationDescription mpd) {
|
||||
return buildDashChunkSource(mpd, false, 0);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
private DashChunkSource setupDashChunkSource(MediaPresentationDescription mpd, long periodStartMs,
|
||||
long liveEdgeLatencyMs, long nowUs) {
|
||||
private static DashChunkSource buildDashChunkSource(MediaPresentationDescription mpd,
|
||||
boolean startAtLiveEdge, long liveEdgeLatencyMs) {
|
||||
@SuppressWarnings("unchecked")
|
||||
ManifestFetcher<MediaPresentationDescription> manifestFetcher = mock(ManifestFetcher.class);
|
||||
when(manifestFetcher.getManifest()).thenReturn(mpd);
|
||||
DashChunkSource chunkSource = new DashChunkSource(manifestFetcher, mpd,
|
||||
AdaptationSet.TYPE_VIDEO, null, mockDataSource, EVALUATOR,
|
||||
new FakeClock(nowUs), liveEdgeLatencyMs * 1000, AVAILABILITY_REALTIME_OFFSET_MS * 1000,
|
||||
false, null, null);
|
||||
AdaptationSet.TYPE_VIDEO, null, mock(DataSource.class), EVALUATOR,
|
||||
new FakeClock(mpd.availabilityStartTime + mpd.duration - ELAPSED_REALTIME_OFFSET_MS),
|
||||
liveEdgeLatencyMs * 1000, ELAPSED_REALTIME_OFFSET_MS * 1000, startAtLiveEdge, null, null);
|
||||
chunkSource.enable(0);
|
||||
return chunkSource;
|
||||
}
|
||||
|
||||
private void checkAvailableRange(TimeRange seekRange, long startTimeUs, long endTimeUs) {
|
||||
private static void checkAvailableRange(TimeRange seekRange, long startTimeUs, long endTimeUs) {
|
||||
long[] seekRangeValuesUs = seekRange.getCurrentBoundsUs(null);
|
||||
assertEquals(startTimeUs, seekRangeValuesUs[0]);
|
||||
assertEquals(endTimeUs, seekRangeValuesUs[1]);
|
||||
}
|
||||
|
||||
private void checkLiveEdgeLatency(DashChunkSource chunkSource, List<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,
|
||||
long availableRangeEndMs, long chunkStartTimeMs, long chunkEndTimeMs) {
|
||||
chunkSource.getChunkOperation(queue, seekPositionMs * 1000, 0, out);
|
||||
TimeRange availableRange = chunkSource.getAvailableRange();
|
||||
|
||||
checkAvailableRange(availableRange, availableRangeStartMs * 1000, availableRangeEndMs * 1000);
|
||||
if (chunkStartTimeMs < availableRangeEndMs) {
|
||||
assertNotNull(out.chunk);
|
||||
@ -564,62 +397,7 @@ public class DashChunkSourceTest extends InstrumentationTestCase {
|
||||
}
|
||||
}
|
||||
|
||||
private void checkLiveEdgeLatency(MediaPresentationDescription mpd, long periodStartMs,
|
||||
long liveEdgeLatencyMs, long seekPositionMs, long availableRangeStartMs,
|
||||
long availableRangeEndMs, long chunkStartTimeMs, long chunkEndTimeMs) {
|
||||
DashChunkSource chunkSource = setupDashChunkSource(mpd, periodStartMs, liveEdgeLatencyMs);
|
||||
List<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) {
|
||||
private static void checkSegmentRequestSequenceOnMultiPeriodLive(DashChunkSource chunkSource) {
|
||||
List<MediaChunk> queue = new ArrayList<>();
|
||||
ChunkOperationHolder out = new ChunkOperationHolder();
|
||||
|
||||
@ -630,7 +408,7 @@ public class DashChunkSourceTest extends InstrumentationTestCase {
|
||||
long chunkEndTimeMs = 1000;
|
||||
|
||||
// request first chunk
|
||||
checkLiveEdgeLatency(chunkSource, queue, out, seekPositionMs,
|
||||
checkLiveEdgeConsistency(chunkSource, queue, out, seekPositionMs,
|
||||
availableRangeStartMs, availableRangeEndMs, chunkStartTimeMs, chunkEndTimeMs);
|
||||
queue.add((MediaChunk) out.chunk);
|
||||
|
||||
@ -638,7 +416,7 @@ public class DashChunkSourceTest extends InstrumentationTestCase {
|
||||
chunkStartTimeMs += 1000;
|
||||
chunkEndTimeMs += 1000;
|
||||
out.chunk = null;
|
||||
checkLiveEdgeLatency(chunkSource, queue, out, seekPositionMs,
|
||||
checkLiveEdgeConsistency(chunkSource, queue, out, seekPositionMs,
|
||||
availableRangeStartMs, availableRangeEndMs, chunkStartTimeMs, chunkEndTimeMs);
|
||||
queue.add((MediaChunk) out.chunk);
|
||||
|
||||
@ -646,7 +424,7 @@ public class DashChunkSourceTest extends InstrumentationTestCase {
|
||||
chunkStartTimeMs += 1000;
|
||||
chunkEndTimeMs += 1000;
|
||||
out.chunk = null;
|
||||
checkLiveEdgeLatency(chunkSource, queue, out, seekPositionMs,
|
||||
checkLiveEdgeConsistency(chunkSource, queue, out, seekPositionMs,
|
||||
availableRangeStartMs, availableRangeEndMs, chunkStartTimeMs, chunkEndTimeMs);
|
||||
queue.add((MediaChunk) out.chunk);
|
||||
|
||||
@ -654,7 +432,7 @@ public class DashChunkSourceTest extends InstrumentationTestCase {
|
||||
chunkStartTimeMs += 1000;
|
||||
chunkEndTimeMs += 1000;
|
||||
out.chunk = null;
|
||||
checkLiveEdgeLatency(chunkSource, queue, out, seekPositionMs,
|
||||
checkLiveEdgeConsistency(chunkSource, queue, out, seekPositionMs,
|
||||
availableRangeStartMs, availableRangeEndMs, chunkStartTimeMs, chunkEndTimeMs);
|
||||
queue.add((MediaChunk) out.chunk);
|
||||
|
||||
@ -662,7 +440,7 @@ public class DashChunkSourceTest extends InstrumentationTestCase {
|
||||
chunkStartTimeMs += 1000;
|
||||
chunkEndTimeMs += 1000;
|
||||
out.chunk = null;
|
||||
checkLiveEdgeLatency(chunkSource, queue, out, seekPositionMs,
|
||||
checkLiveEdgeConsistency(chunkSource, queue, out, seekPositionMs,
|
||||
availableRangeStartMs, availableRangeEndMs, chunkStartTimeMs, chunkEndTimeMs);
|
||||
queue.add((MediaChunk) out.chunk);
|
||||
|
||||
@ -670,7 +448,7 @@ public class DashChunkSourceTest extends InstrumentationTestCase {
|
||||
chunkStartTimeMs += 1000;
|
||||
chunkEndTimeMs += 1000;
|
||||
out.chunk = null;
|
||||
checkLiveEdgeLatency(chunkSource, queue, out, seekPositionMs,
|
||||
checkLiveEdgeConsistency(chunkSource, queue, out, seekPositionMs,
|
||||
availableRangeStartMs, availableRangeEndMs, chunkStartTimeMs, chunkEndTimeMs);
|
||||
queue.add((MediaChunk) out.chunk);
|
||||
|
||||
@ -678,7 +456,7 @@ public class DashChunkSourceTest extends InstrumentationTestCase {
|
||||
chunkStartTimeMs += 1000;
|
||||
chunkEndTimeMs += 1000;
|
||||
out.chunk = null;
|
||||
checkLiveEdgeLatency(chunkSource, queue, out, seekPositionMs,
|
||||
checkLiveEdgeConsistency(chunkSource, queue, out, seekPositionMs,
|
||||
availableRangeStartMs, availableRangeEndMs, chunkStartTimeMs, chunkEndTimeMs);
|
||||
queue.add((MediaChunk) out.chunk);
|
||||
|
||||
@ -686,7 +464,7 @@ public class DashChunkSourceTest extends InstrumentationTestCase {
|
||||
chunkStartTimeMs += 1000;
|
||||
chunkEndTimeMs += 1000;
|
||||
out.chunk = null;
|
||||
checkLiveEdgeLatency(chunkSource, queue, out, seekPositionMs,
|
||||
checkLiveEdgeConsistency(chunkSource, queue, out, seekPositionMs,
|
||||
availableRangeStartMs, availableRangeEndMs, chunkStartTimeMs, chunkEndTimeMs);
|
||||
queue.add((MediaChunk) out.chunk);
|
||||
|
||||
@ -694,7 +472,7 @@ public class DashChunkSourceTest extends InstrumentationTestCase {
|
||||
chunkStartTimeMs += 1000;
|
||||
chunkEndTimeMs += 1000;
|
||||
out.chunk = null;
|
||||
checkLiveEdgeLatency(chunkSource, queue, out, seekPositionMs,
|
||||
checkLiveEdgeConsistency(chunkSource, queue, out, seekPositionMs,
|
||||
availableRangeStartMs, availableRangeEndMs, chunkStartTimeMs, chunkEndTimeMs);
|
||||
queue.add((MediaChunk) out.chunk);
|
||||
|
||||
@ -702,7 +480,7 @@ public class DashChunkSourceTest extends InstrumentationTestCase {
|
||||
chunkStartTimeMs += 1000;
|
||||
chunkEndTimeMs += 1000;
|
||||
out.chunk = null;
|
||||
checkLiveEdgeLatency(chunkSource, queue, out, seekPositionMs,
|
||||
checkLiveEdgeConsistency(chunkSource, queue, out, seekPositionMs,
|
||||
availableRangeStartMs, availableRangeEndMs, chunkStartTimeMs, chunkEndTimeMs);
|
||||
queue.add((MediaChunk) out.chunk);
|
||||
|
||||
|
@ -30,12 +30,11 @@ public class RepresentationTest extends TestCase {
|
||||
String uri = "http://www.google.com";
|
||||
SegmentBase base = new SingleSegmentBase(new RangedUri(uri, null, 0, 1), 1, 0, uri, 1, 1);
|
||||
Format format = new Format("0", MimeTypes.VIDEO_MP4, 1920, 1080, -1, 0, 0, 2500000);
|
||||
Representation representation = Representation.newInstance(-1, -1, "test_stream_1", 3,
|
||||
format, base);
|
||||
Representation representation = Representation.newInstance("test_stream_1", 3, format, base);
|
||||
assertEquals("test_stream_1.0.3", representation.getCacheKey());
|
||||
|
||||
format = new Format("150", MimeTypes.VIDEO_MP4, 1920, 1080, -1, 0, 0, 2500000);
|
||||
representation = Representation.newInstance(-1, -1, "test_stream_1", -1, format, base);
|
||||
representation = Representation.newInstance("test_stream_1", -1, format, base);
|
||||
assertEquals("test_stream_1.150.-1", representation.getCacheKey());
|
||||
}
|
||||
|
||||
|
@ -16,11 +16,11 @@
|
||||
package com.google.android.exoplayer.dash;
|
||||
|
||||
import com.google.android.exoplayer.BehindLiveWindowException;
|
||||
import com.google.android.exoplayer.C;
|
||||
import com.google.android.exoplayer.MediaFormat;
|
||||
import com.google.android.exoplayer.TimeRange;
|
||||
import com.google.android.exoplayer.TimeRange.DynamicTimeRange;
|
||||
import com.google.android.exoplayer.TimeRange.StaticTimeRange;
|
||||
import com.google.android.exoplayer.TrackRenderer;
|
||||
import com.google.android.exoplayer.chunk.Chunk;
|
||||
import com.google.android.exoplayer.chunk.ChunkExtractorWrapper;
|
||||
import com.google.android.exoplayer.chunk.ChunkOperationHolder;
|
||||
@ -141,11 +141,13 @@ public class DashChunkSource implements ChunkSource {
|
||||
*
|
||||
* @param dataSource A {@link DataSource} suitable for loading the media data.
|
||||
* @param formatEvaluator Selects from the available formats.
|
||||
* @param durationMs The duration of the content.
|
||||
* @param representations The representations to be considered by the source.
|
||||
*/
|
||||
public DashChunkSource(DataSource dataSource, FormatEvaluator formatEvaluator,
|
||||
Representation... representations) {
|
||||
this(buildManifest(Arrays.asList(representations)), 0, null, dataSource, formatEvaluator);
|
||||
long durationMs, Representation... representations) {
|
||||
this(buildManifest(durationMs, Arrays.asList(representations)), 0, null, dataSource,
|
||||
formatEvaluator);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -153,11 +155,12 @@ public class DashChunkSource implements ChunkSource {
|
||||
*
|
||||
* @param dataSource A {@link DataSource} suitable for loading the media data.
|
||||
* @param formatEvaluator Selects from the available formats.
|
||||
* @param durationMs The duration of the content.
|
||||
* @param representations The representations to be considered by the source.
|
||||
*/
|
||||
public DashChunkSource(DataSource dataSource, FormatEvaluator formatEvaluator,
|
||||
List<Representation> representations) {
|
||||
this(buildManifest(representations), 0, null, dataSource, formatEvaluator);
|
||||
long durationMs, List<Representation> representations) {
|
||||
this(buildManifest(durationMs, representations), 0, null, dataSource, formatEvaluator);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -277,9 +280,9 @@ public class DashChunkSource implements ChunkSource {
|
||||
String mimeType = "";
|
||||
for (int i = 0; i < periodHolders.size(); i++) {
|
||||
PeriodHolder periodHolder = periodHolders.valueAt(i);
|
||||
if (totalDurationUs != TrackRenderer.UNKNOWN_TIME_US) {
|
||||
if (periodHolder.durationUs == TrackRenderer.UNKNOWN_TIME_US) {
|
||||
totalDurationUs = TrackRenderer.UNKNOWN_TIME_US;
|
||||
if (totalDurationUs != C.UNKNOWN_TIME_US) {
|
||||
if (periodHolder.durationUs == C.UNKNOWN_TIME_US) {
|
||||
totalDurationUs = C.UNKNOWN_TIME_US;
|
||||
} else {
|
||||
totalDurationUs += periodHolder.durationUs;
|
||||
}
|
||||
@ -394,9 +397,7 @@ public class DashChunkSource implements ChunkSource {
|
||||
// In all cases where we return before instantiating a new chunk, we want out.chunk to be null.
|
||||
out.chunk = null;
|
||||
|
||||
long segmentStartTimeUs;
|
||||
int segmentNum = -1;
|
||||
boolean startingNewPeriod = false;
|
||||
boolean startingNewPeriod;
|
||||
PeriodHolder periodHolder;
|
||||
|
||||
availableRange.getCurrentBoundsUs(availableRangeValues);
|
||||
@ -415,7 +416,6 @@ public class DashChunkSource implements ChunkSource {
|
||||
}
|
||||
|
||||
periodHolder = findPeriodHolder(seekPositionUs);
|
||||
segmentStartTimeUs = seekPositionUs;
|
||||
startingNewPeriod = true;
|
||||
} else {
|
||||
if (startAtLiveEdge) {
|
||||
@ -430,15 +430,13 @@ public class DashChunkSource implements ChunkSource {
|
||||
return;
|
||||
}
|
||||
|
||||
segmentNum = previous.chunkIndex + 1;
|
||||
segmentStartTimeUs = previous.endTimeUs;
|
||||
|
||||
if (currentManifest.dynamic) {
|
||||
if (segmentStartTimeUs < availableRangeValues[0]) {
|
||||
long nextSegmentStartTimeUs = previous.endTimeUs;
|
||||
if (nextSegmentStartTimeUs < availableRangeValues[0]) {
|
||||
// This is before the first chunk in the current manifest.
|
||||
fatalError = new BehindLiveWindowException();
|
||||
return;
|
||||
} else if (segmentStartTimeUs >= availableRangeValues[1]) {
|
||||
} else if (nextSegmentStartTimeUs >= availableRangeValues[1]) {
|
||||
// This chunk is beyond the last chunk in the current manifest. If the index is bounded
|
||||
// we'll need to wait until it's refreshed. If it's unbounded we just need to wait for a
|
||||
// while before attempting to load the chunk.
|
||||
@ -446,21 +444,23 @@ public class DashChunkSource implements ChunkSource {
|
||||
}
|
||||
}
|
||||
|
||||
startingNewPeriod = false;
|
||||
periodHolder = periodHolders.get(previous.parentId);
|
||||
if (periodHolder == null) {
|
||||
// the previous chunk was from a period that's no longer on the manifest, therefore the
|
||||
// The previous chunk was from a period that's no longer on the manifest, therefore the
|
||||
// next chunk must be the first one in the first period that's still on the manifest
|
||||
// (note that we can't actually update the segmentNum yet because the new period might
|
||||
// have a different sequence and it's segmentIndex might not have been loaded yet)
|
||||
// have a different sequence and it's segmentIndex might not have been loaded yet).
|
||||
periodHolder = periodHolders.valueAt(0);
|
||||
startingNewPeriod = true;
|
||||
} else if (!periodHolder.isIndexUnbounded()
|
||||
&& segmentStartTimeUs >= periodHolder.getAvailableEndTimeUs()) {
|
||||
// we reached the end of a period, start the next one (note that we can't actually
|
||||
// update the segmentNum yet because the new period might have a different
|
||||
// sequence and it's segmentIndex might not have been loaded yet)
|
||||
periodHolder = periodHolders.get(previous.parentId + 1);
|
||||
startingNewPeriod = true;
|
||||
} else if (!periodHolder.isIndexUnbounded()) {
|
||||
RepresentationHolder representationHolder =
|
||||
periodHolder.representationHolders.get(previous.format.id);
|
||||
if (representationHolder.isLastSegment(previous.chunkIndex)) {
|
||||
// We reached the end of a period. Start the next one.
|
||||
periodHolder = periodHolders.get(previous.parentId + 1);
|
||||
startingNewPeriod = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -483,24 +483,16 @@ public class DashChunkSource implements ChunkSource {
|
||||
if (pendingInitializationUri != null || pendingIndexUri != null) {
|
||||
// We have initialization and/or index requests to make.
|
||||
Chunk initializationChunk = newInitializationChunk(pendingInitializationUri, pendingIndexUri,
|
||||
selectedRepresentation, extractorWrapper, dataSource, periodHolder.manifestIndex,
|
||||
selectedRepresentation, extractorWrapper, dataSource, periodHolder.localIndex,
|
||||
evaluation.trigger);
|
||||
lastChunkWasInitialization = true;
|
||||
out.chunk = initializationChunk;
|
||||
return;
|
||||
}
|
||||
|
||||
if (startingNewPeriod) {
|
||||
if (queue.isEmpty()) {
|
||||
// when starting a new period (or beginning playback for the first time), the segment
|
||||
// numbering might have been reset, so we'll need to determine the correct number from
|
||||
// the representation holder itself
|
||||
segmentNum = representationHolder.getSegmentNum(segmentStartTimeUs);
|
||||
} else {
|
||||
segmentNum = representationHolder.getFirstAvailableSegmentNum();
|
||||
}
|
||||
}
|
||||
|
||||
int segmentNum = queue.isEmpty() ? representationHolder.getSegmentNum(seekPositionUs)
|
||||
: startingNewPeriod ? representationHolder.getFirstAvailableSegmentNum()
|
||||
: queue.get(out.queueSize - 1).chunkIndex + 1;
|
||||
Chunk nextMediaChunk = newMediaChunk(periodHolder, representationHolder, dataSource,
|
||||
mediaFormat, segmentNum, evaluation.trigger);
|
||||
lastChunkWasInitialization = false;
|
||||
@ -534,8 +526,7 @@ public class DashChunkSource implements ChunkSource {
|
||||
if (initializationChunk.hasSeekMap()) {
|
||||
representationHolder.segmentIndex = new DashWrappingSegmentIndex(
|
||||
(ChunkIndex) initializationChunk.getSeekMap(),
|
||||
initializationChunk.dataSpec.uri.toString(),
|
||||
periodHolder.startTimeUs);
|
||||
initializationChunk.dataSpec.uri.toString());
|
||||
}
|
||||
|
||||
// The null check avoids overwriting drmInitData obtained from the manifest with drmInitData
|
||||
@ -594,13 +585,13 @@ public class DashChunkSource implements ChunkSource {
|
||||
return new SingleSampleMediaChunk(dataSource, dataSpec, Chunk.TRIGGER_INITIAL,
|
||||
representation.format, startTimeUs, endTimeUs, segmentNum, isLastSegment,
|
||||
MediaFormat.createTextFormat(MimeTypes.TEXT_VTT, MediaFormat.NO_VALUE,
|
||||
representation.format.language), null, periodHolder.manifestIndex);
|
||||
representation.format.language), null, periodHolder.localIndex);
|
||||
} else {
|
||||
boolean isMediaFormatFinal = (mediaFormat != null);
|
||||
return new ContainerMediaChunk(dataSource, dataSpec, trigger, representation.format,
|
||||
startTimeUs, endTimeUs, segmentNum, isLastSegment, sampleOffsetUs,
|
||||
representationHolder.extractorWrapper, mediaFormat, maxWidth, maxHeight, drmInitData,
|
||||
isMediaFormatFinal, periodHolder.manifestIndex);
|
||||
isMediaFormatFinal, periodHolder.localIndex);
|
||||
}
|
||||
}
|
||||
|
||||
@ -624,7 +615,7 @@ public class DashChunkSource implements ChunkSource {
|
||||
|
||||
private static DrmInitData getDrmInitData(MediaPresentationDescription manifest,
|
||||
int adaptationSetIndex) {
|
||||
AdaptationSet adaptationSet = manifest.periods.get(0).adaptationSets.get(adaptationSetIndex);
|
||||
AdaptationSet adaptationSet = manifest.getPeriod(0).adaptationSets.get(adaptationSetIndex);
|
||||
String drmInitMimeType = mimeTypeIsWebm(adaptationSet.representations.get(0).format.mimeType)
|
||||
? MimeTypes.VIDEO_WEBM : MimeTypes.VIDEO_MP4;
|
||||
if (adaptationSet.contentProtections.isEmpty()) {
|
||||
@ -644,13 +635,11 @@ public class DashChunkSource implements ChunkSource {
|
||||
}
|
||||
}
|
||||
|
||||
private static MediaPresentationDescription buildManifest(List<Representation> representations) {
|
||||
Representation firstRepresentation = representations.get(0);
|
||||
private static MediaPresentationDescription buildManifest(long durationMs,
|
||||
List<Representation> representations) {
|
||||
AdaptationSet adaptationSet = new AdaptationSet(0, AdaptationSet.TYPE_UNKNOWN, representations);
|
||||
Period period = new Period(null, firstRepresentation.periodStartMs,
|
||||
firstRepresentation.periodDurationMs, Collections.singletonList(adaptationSet));
|
||||
long duration = firstRepresentation.periodDurationMs - firstRepresentation.periodStartMs;
|
||||
return new MediaPresentationDescription(-1, duration, -1, false, -1, -1, null, null,
|
||||
Period period = new Period(null, 0, Collections.singletonList(adaptationSet));
|
||||
return new MediaPresentationDescription(-1, durationMs, -1, false, -1, -1, null, null,
|
||||
Collections.singletonList(period));
|
||||
}
|
||||
|
||||
@ -672,21 +661,19 @@ public class DashChunkSource implements ChunkSource {
|
||||
}
|
||||
|
||||
private void processManifest(MediaPresentationDescription manifest) {
|
||||
List<Period> newPeriods = manifest.periods;
|
||||
|
||||
// Remove old periods.
|
||||
Period firstPeriod = newPeriods.get(0);
|
||||
Period firstPeriod = manifest.getPeriod(0);
|
||||
while (periodHolders.size() > 0
|
||||
&& periodHolders.valueAt(0).startTimeUs < firstPeriod.startMs * 1000) {
|
||||
PeriodHolder periodHolder = periodHolders.valueAt(0);
|
||||
// TODO: Use periodHolders.removeAt(0) if the minimum API level is ever increased to 11.
|
||||
periodHolders.remove(periodHolder.manifestIndex);
|
||||
periodHolders.remove(periodHolder.localIndex);
|
||||
}
|
||||
|
||||
// Update existing periods.
|
||||
try {
|
||||
for (int i = 0; i < periodHolders.size(); i++) {
|
||||
periodHolders.valueAt(i).updatePeriod(newPeriods.get(i));
|
||||
periodHolders.valueAt(i).updatePeriod(manifest, i);
|
||||
}
|
||||
} catch (BehindLiveWindowException e) {
|
||||
fatalError = e;
|
||||
@ -694,9 +681,8 @@ public class DashChunkSource implements ChunkSource {
|
||||
}
|
||||
|
||||
// Add new periods.
|
||||
for (int i = periodHolders.size(); i < newPeriods.size(); i++) {
|
||||
Period period = newPeriods.get(i);
|
||||
PeriodHolder periodHolder = new PeriodHolder(periodHolderNextIndex, period,
|
||||
for (int i = periodHolders.size(); i < manifest.getPeriodCount(); i++) {
|
||||
PeriodHolder periodHolder = new PeriodHolder(periodHolderNextIndex, manifest, i,
|
||||
adaptationSetIndex, representationIndices);
|
||||
periodHolders.put(periodHolderNextIndex, periodHolder);
|
||||
periodHolderNextIndex++;
|
||||
@ -725,7 +711,7 @@ public class DashChunkSource implements ChunkSource {
|
||||
long maxEndPositionUs = lastPeriod.isIndexUnbounded() ? Long.MAX_VALUE
|
||||
: lastPeriod.getAvailableEndTimeUs();
|
||||
long elapsedRealtimeAtZeroUs = (systemClock.elapsedRealtime() * 1000)
|
||||
- (nowUs - currentManifest.availabilityStartTime * 1000);
|
||||
- (nowUs - (currentManifest.availabilityStartTime * 1000));
|
||||
long timeShiftBufferDepthUs = currentManifest.timeShiftBufferDepth == -1 ? -1
|
||||
: currentManifest.timeShiftBufferDepth * 1000;
|
||||
return new DynamicTimeRange(minStartPositionUs, maxEndPositionUs, elapsedRealtimeAtZeroUs,
|
||||
@ -751,58 +737,76 @@ public class DashChunkSource implements ChunkSource {
|
||||
public DashSegmentIndex segmentIndex;
|
||||
public MediaFormat mediaFormat;
|
||||
|
||||
private final long periodStartTimeUs;
|
||||
private long periodDurationUs;
|
||||
|
||||
private int segmentNumShift;
|
||||
|
||||
public RepresentationHolder(Representation representation,
|
||||
ChunkExtractorWrapper extractorWrapper) {
|
||||
public RepresentationHolder(long periodStartTimeUs, long periodDurationUs,
|
||||
Representation representation, ChunkExtractorWrapper extractorWrapper) {
|
||||
this.periodStartTimeUs = periodStartTimeUs;
|
||||
this.periodDurationUs = periodDurationUs;
|
||||
this.representation = representation;
|
||||
this.extractorWrapper = extractorWrapper;
|
||||
this.segmentIndex = representation.getIndex();
|
||||
segmentIndex = representation.getIndex();
|
||||
}
|
||||
|
||||
public void updateRepresentation(Representation newRepresentation)
|
||||
public void updateRepresentation(long newPeriodDurationUs, Representation newRepresentation)
|
||||
throws BehindLiveWindowException{
|
||||
DashSegmentIndex oldIndex = representation.getIndex();
|
||||
DashSegmentIndex newIndex = newRepresentation.getIndex();
|
||||
|
||||
periodDurationUs = newPeriodDurationUs;
|
||||
representation = newRepresentation;
|
||||
if (newIndex == null) {
|
||||
if (oldIndex == null) {
|
||||
// Segment numbers cannot shift if the index isn't defined by the manifest.
|
||||
return;
|
||||
}
|
||||
|
||||
segmentIndex = newIndex;
|
||||
int oldIndexLastSegmentNum = oldIndex.getLastSegmentNum();
|
||||
if (!oldIndex.isExplicit()) {
|
||||
// Segment numbers cannot shift if the index isn't explicit.
|
||||
return;
|
||||
}
|
||||
|
||||
int oldIndexLastSegmentNum = oldIndex.getLastSegmentNum(periodDurationUs);
|
||||
long oldIndexEndTimeUs = oldIndex.getTimeUs(oldIndexLastSegmentNum)
|
||||
+ oldIndex.getDurationUs(oldIndexLastSegmentNum);
|
||||
+ oldIndex.getDurationUs(oldIndexLastSegmentNum, periodDurationUs);
|
||||
int newIndexFirstSegmentNum = newIndex.getFirstSegmentNum();
|
||||
long newIndexStartTimeUs = newIndex.getTimeUs(newIndexFirstSegmentNum);
|
||||
if (oldIndexEndTimeUs == newIndexStartTimeUs) {
|
||||
// The new manifest continues where the old one ended, with no overlap.
|
||||
segmentNumShift += oldIndex.getLastSegmentNum() + 1 - newIndexFirstSegmentNum;
|
||||
segmentNumShift += oldIndex.getLastSegmentNum(periodDurationUs) + 1
|
||||
- newIndexFirstSegmentNum;
|
||||
} else if (oldIndexEndTimeUs < newIndexStartTimeUs) {
|
||||
// There's a gap between the old manifest and the new one which means we've slipped
|
||||
// behind the live window and can't proceed.
|
||||
throw new BehindLiveWindowException();
|
||||
} else {
|
||||
// The new manifest overlaps with the old one.
|
||||
segmentNumShift += oldIndex.getSegmentNum(newIndexStartTimeUs) - newIndexFirstSegmentNum;
|
||||
segmentNumShift += oldIndex.getSegmentNum(newIndexStartTimeUs, periodDurationUs)
|
||||
- newIndexFirstSegmentNum;
|
||||
}
|
||||
}
|
||||
|
||||
public int getSegmentNum(long positionUs) {
|
||||
return segmentIndex.getSegmentNum(positionUs) + segmentNumShift;
|
||||
return segmentIndex.getSegmentNum(positionUs - periodStartTimeUs, periodDurationUs)
|
||||
+ segmentNumShift;
|
||||
}
|
||||
|
||||
public long getSegmentStartTimeUs(int segmentNum) {
|
||||
return segmentIndex.getTimeUs(segmentNum - segmentNumShift);
|
||||
return segmentIndex.getTimeUs(segmentNum - segmentNumShift) + periodStartTimeUs;
|
||||
}
|
||||
|
||||
public long getSegmentEndTimeUs(int segmentNum) {
|
||||
return getSegmentStartTimeUs(segmentNum)
|
||||
+ segmentIndex.getDurationUs(segmentNum - segmentNumShift);
|
||||
+ segmentIndex.getDurationUs(segmentNum - segmentNumShift, periodDurationUs);
|
||||
}
|
||||
|
||||
public boolean isLastSegment(int segmentNum) {
|
||||
return (segmentNum - segmentNumShift) == segmentIndex.getLastSegmentNum();
|
||||
int lastSegmentNum = segmentIndex.getLastSegmentNum(periodDurationUs);
|
||||
return lastSegmentNum == DashSegmentIndex.INDEX_UNBOUNDED ? false
|
||||
: segmentNum == (lastSegmentNum + segmentNumShift);
|
||||
}
|
||||
|
||||
public int getFirstAvailableSegmentNum() {
|
||||
@ -817,7 +821,7 @@ public class DashChunkSource implements ChunkSource {
|
||||
|
||||
private static final class PeriodHolder {
|
||||
|
||||
public final int manifestIndex;
|
||||
public final int localIndex;
|
||||
public final long startTimeUs;
|
||||
public final long durationUs;
|
||||
public final String mimeType;
|
||||
@ -834,12 +838,16 @@ public class DashChunkSource implements ChunkSource {
|
||||
private long availableStartTimeUs;
|
||||
private long availableEndTimeUs;
|
||||
|
||||
public PeriodHolder(int manifestIndex, Period period, int adaptationSetIndex,
|
||||
int[] representationIndices) {
|
||||
this.manifestIndex = manifestIndex;
|
||||
public PeriodHolder(int localIndex, MediaPresentationDescription manifest,
|
||||
int manifestIndex, int adaptationSetIndex, int[] representationIndices) {
|
||||
this.localIndex = localIndex;
|
||||
this.adaptationSetIndex = adaptationSetIndex;
|
||||
this.representationIndices = representationIndices;
|
||||
|
||||
Period period = manifest.getPeriod(manifestIndex);
|
||||
startTimeUs = period.startMs * 1000;
|
||||
durationUs = getPeriodDurationUs(manifest, manifestIndex);
|
||||
|
||||
List<Representation> periodRepresentations =
|
||||
period.adaptationSets.get(adaptationSetIndex).representations;
|
||||
int representationCount = representationIndices != null ? representationIndices.length
|
||||
@ -858,35 +866,32 @@ public class DashChunkSource implements ChunkSource {
|
||||
maxWidth = Math.max(formats[i].width, maxWidth);
|
||||
maxHeight = Math.max(formats[i].height, maxHeight);
|
||||
Extractor extractor = mimeTypeIsWebm(formats[i].mimeType) ? new WebmExtractor()
|
||||
: new FragmentedMp4Extractor();
|
||||
RepresentationHolder representationHolder =
|
||||
new RepresentationHolder(representation, new ChunkExtractorWrapper(extractor));
|
||||
: new FragmentedMp4Extractor();
|
||||
RepresentationHolder representationHolder = new RepresentationHolder(startTimeUs,
|
||||
durationUs, representation, new ChunkExtractorWrapper(extractor));
|
||||
representationHolders.put(formats[i].id, representationHolder);
|
||||
}
|
||||
this.maxWidth = maxWidth;
|
||||
this.maxHeight = maxHeight;
|
||||
this.mimeType = mimeType;
|
||||
|
||||
startTimeUs = period.startMs * 1000;
|
||||
long durationMs = period.durationMs;
|
||||
if (durationMs == -1) {
|
||||
durationUs = TrackRenderer.UNKNOWN_TIME_US;
|
||||
} else {
|
||||
durationUs = durationMs * 1000;
|
||||
}
|
||||
|
||||
Arrays.sort(formats, new DecreasingBandwidthComparator());
|
||||
updateRepresentationIndependentProperties();
|
||||
}
|
||||
|
||||
public void updatePeriod(Period period) throws BehindLiveWindowException {
|
||||
public void updatePeriod(MediaPresentationDescription manifest, int manifestIndex)
|
||||
throws BehindLiveWindowException {
|
||||
Period period = manifest.getPeriod(manifestIndex);
|
||||
long durationUs = getPeriodDurationUs(manifest, manifestIndex);
|
||||
|
||||
List<Representation> representations =
|
||||
period.adaptationSets.get(adaptationSetIndex).representations;
|
||||
int representationCount = formats.length;
|
||||
for (int i = 0; i < representationCount; i++) {
|
||||
int representationIndex = representationIndices != null ? representationIndices[i] : i;
|
||||
Representation representation = representations.get(representationIndex);
|
||||
representationHolders.get(representation.format.id).updateRepresentation(representation);
|
||||
representationHolders.get(representation.format.id).updateRepresentation(durationUs,
|
||||
representation);
|
||||
}
|
||||
updateRepresentationIndependentProperties();
|
||||
}
|
||||
@ -915,13 +920,14 @@ public class DashChunkSource implements ChunkSource {
|
||||
Representation representation = representationHolders.get(formats[0].id).representation;
|
||||
DashSegmentIndex segmentIndex = representation.getIndex();
|
||||
if (segmentIndex != null) {
|
||||
int lastSegmentNum = segmentIndex.getLastSegmentNum();
|
||||
int firstSegmentNum = segmentIndex.getFirstSegmentNum();
|
||||
int lastSegmentNum = segmentIndex.getLastSegmentNum(durationUs);
|
||||
indexIsUnbounded = lastSegmentNum == DashSegmentIndex.INDEX_UNBOUNDED;
|
||||
indexIsExplicit = segmentIndex.isExplicit();
|
||||
availableStartTimeUs = segmentIndex.getTimeUs(segmentIndex.getFirstSegmentNum());
|
||||
availableStartTimeUs = startTimeUs + segmentIndex.getTimeUs(firstSegmentNum);
|
||||
if (!indexIsUnbounded) {
|
||||
availableEndTimeUs = segmentIndex.getTimeUs(lastSegmentNum)
|
||||
+ segmentIndex.getDurationUs(lastSegmentNum);
|
||||
availableEndTimeUs = startTimeUs + segmentIndex.getTimeUs(lastSegmentNum)
|
||||
+ segmentIndex.getDurationUs(lastSegmentNum, durationUs);
|
||||
}
|
||||
} else {
|
||||
indexIsUnbounded = false;
|
||||
@ -931,6 +937,15 @@ public class DashChunkSource implements ChunkSource {
|
||||
}
|
||||
}
|
||||
|
||||
private static long getPeriodDurationUs(MediaPresentationDescription manifest, int index) {
|
||||
long durationMs = manifest.getPeriodDuration(index);
|
||||
if (durationMs == -1) {
|
||||
return C.UNKNOWN_TIME_US;
|
||||
} else {
|
||||
return durationMs * 1000;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -15,6 +15,7 @@
|
||||
*/
|
||||
package com.google.android.exoplayer.dash;
|
||||
|
||||
import com.google.android.exoplayer.C;
|
||||
import com.google.android.exoplayer.dash.mpd.RangedUri;
|
||||
|
||||
/**
|
||||
@ -24,20 +25,22 @@ import com.google.android.exoplayer.dash.mpd.RangedUri;
|
||||
*/
|
||||
public interface DashSegmentIndex {
|
||||
|
||||
public static final int INDEX_UNBOUNDED = -1;
|
||||
int INDEX_UNBOUNDED = -1;
|
||||
|
||||
/**
|
||||
* Returns the segment number of the segment containing a given media time.
|
||||
* <p>
|
||||
* If the given media time is outside the range of the index, then the returned segment number is
|
||||
* clamped to {@link #getFirstSegmentNum()} (if the given media time is earlier the start of the
|
||||
* first segment) or {@link #getLastSegmentNum()} (if the given media time is later then the end
|
||||
* of the last segment).
|
||||
* first segment) or {@link #getLastSegmentNum(long)} (if the given media time is later then the
|
||||
* end of the last segment).
|
||||
*
|
||||
* @param timeUs The time in microseconds.
|
||||
* @param periodDurationUs The duration of the enclosing period in microseconds, or
|
||||
* {@link C#UNKNOWN_TIME_US} if the period's duration is not yet known.
|
||||
* @return The segment number of the corresponding segment.
|
||||
*/
|
||||
int getSegmentNum(long timeUs);
|
||||
int getSegmentNum(long timeUs, long periodDurationUs);
|
||||
|
||||
/**
|
||||
* Returns the start time of a segment.
|
||||
@ -51,9 +54,11 @@ public interface DashSegmentIndex {
|
||||
* Returns the duration of a segment.
|
||||
*
|
||||
* @param segmentNum The segment number.
|
||||
* @param periodDurationUs The duration of the enclosing period in microseconds, or
|
||||
* {@link C#UNKNOWN_TIME_US} if the period's duration is not yet known.
|
||||
* @return The duration of the segment, in microseconds.
|
||||
*/
|
||||
long getDurationUs(int segmentNum);
|
||||
long getDurationUs(int segmentNum, long periodDurationUs);
|
||||
|
||||
/**
|
||||
* Returns a {@link RangedUri} defining the location of a segment.
|
||||
@ -73,15 +78,15 @@ public interface DashSegmentIndex {
|
||||
/**
|
||||
* Returns the segment number of the last segment, or {@link #INDEX_UNBOUNDED}.
|
||||
* <p>
|
||||
* An unbounded index occurs if a live stream manifest uses SegmentTemplate elements without a
|
||||
* SegmentTimeline element. In this case the manifest can be used to derive information about
|
||||
* segments arbitrarily far into the future. This means that the manifest does not need to be
|
||||
* refreshed as frequently (if at all) during playback, however it is necessary for a player to
|
||||
* manually calculate the window of currently available segments.
|
||||
* An unbounded index occurs if a dynamic manifest uses SegmentTemplate elements without a
|
||||
* SegmentTimeline element, and if the period duration is not yet known. In this case the caller
|
||||
* must manually determine the window of currently available segments.
|
||||
*
|
||||
* @param periodDurationUs The duration of the enclosing period in microseconds, or
|
||||
* {@link C#UNKNOWN_TIME_US} if the period's duration is not yet known.
|
||||
* @return The segment number of the last segment, or {@link #INDEX_UNBOUNDED}.
|
||||
*/
|
||||
int getLastSegmentNum();
|
||||
int getLastSegmentNum(long periodDurationUs);
|
||||
|
||||
/**
|
||||
* Returns true if segments are defined explicitly by the index.
|
||||
|
@ -26,17 +26,14 @@ import com.google.android.exoplayer.extractor.ChunkIndex;
|
||||
|
||||
private final ChunkIndex chunkIndex;
|
||||
private final String uri;
|
||||
private final long startTimeUs;
|
||||
|
||||
/**
|
||||
* @param chunkIndex The {@link ChunkIndex} to wrap.
|
||||
* @param uri The URI where the data is located.
|
||||
* @param startTimeUs The start time of the index, in microseconds.
|
||||
*/
|
||||
public DashWrappingSegmentIndex(ChunkIndex chunkIndex, String uri, long startTimeUs) {
|
||||
public DashWrappingSegmentIndex(ChunkIndex chunkIndex, String uri) {
|
||||
this.chunkIndex = chunkIndex;
|
||||
this.uri = uri;
|
||||
this.startTimeUs = startTimeUs;
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -45,17 +42,17 @@ import com.google.android.exoplayer.extractor.ChunkIndex;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getLastSegmentNum() {
|
||||
public int getLastSegmentNum(long periodDurationUs) {
|
||||
return chunkIndex.length - 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getTimeUs(int segmentNum) {
|
||||
return chunkIndex.timesUs[segmentNum] + startTimeUs;
|
||||
return chunkIndex.timesUs[segmentNum];
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getDurationUs(int segmentNum) {
|
||||
public long getDurationUs(int segmentNum, long periodDurationUs) {
|
||||
return chunkIndex.durationsUs[segmentNum];
|
||||
}
|
||||
|
||||
@ -65,8 +62,8 @@ import com.google.android.exoplayer.extractor.ChunkIndex;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getSegmentNum(long timeUs) {
|
||||
return chunkIndex.getChunkIndex(timeUs - startTimeUs);
|
||||
public int getSegmentNum(long timeUs, long periodDurationUs) {
|
||||
return chunkIndex.getChunkIndex(timeUs);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -22,34 +22,28 @@ import com.google.android.exoplayer.dash.DashSegmentIndex;
|
||||
*/
|
||||
/* package */ final class DashSingleSegmentIndex implements DashSegmentIndex {
|
||||
|
||||
private final long startTimeUs;
|
||||
private final long durationUs;
|
||||
private final RangedUri uri;
|
||||
|
||||
/**
|
||||
* @param startTimeUs The start time of the segment, in microseconds.
|
||||
* @param durationUs The duration of the segment, in microseconds.
|
||||
* @param uri A {@link RangedUri} defining the location of the segment data.
|
||||
*/
|
||||
public DashSingleSegmentIndex(long startTimeUs, long durationUs, RangedUri uri) {
|
||||
this.startTimeUs = startTimeUs;
|
||||
this.durationUs = durationUs;
|
||||
public DashSingleSegmentIndex(RangedUri uri) {
|
||||
this.uri = uri;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getSegmentNum(long timeUs) {
|
||||
public int getSegmentNum(long timeUs, long periodDurationUs) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getTimeUs(int segmentNum) {
|
||||
return startTimeUs;
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getDurationUs(int segmentNum) {
|
||||
return durationUs;
|
||||
public long getDurationUs(int segmentNum, long periodDurationUs) {
|
||||
return periodDurationUs;
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -63,7 +57,7 @@ import com.google.android.exoplayer.dash.DashSegmentIndex;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getLastSegmentNum() {
|
||||
public int getLastSegmentNum(long periodDurationUs) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
@ -41,7 +41,7 @@ public class MediaPresentationDescription implements RedirectingManifest {
|
||||
|
||||
public final String location;
|
||||
|
||||
public final List<Period> periods;
|
||||
private final List<Period> periods;
|
||||
|
||||
public MediaPresentationDescription(long availabilityStartTime, long duration, long minBufferTime,
|
||||
boolean dynamic, long minUpdatePeriod, long timeShiftBufferDepth, UtcTimingElement utcTiming,
|
||||
@ -54,12 +54,26 @@ public class MediaPresentationDescription implements RedirectingManifest {
|
||||
this.timeShiftBufferDepth = timeShiftBufferDepth;
|
||||
this.utcTiming = utcTiming;
|
||||
this.location = location;
|
||||
this.periods = Collections.unmodifiableList(periods);
|
||||
this.periods = periods == null ? Collections.<Period>emptyList() : periods;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getNextManifestUri() {
|
||||
public final String getNextManifestUri() {
|
||||
return location;
|
||||
}
|
||||
|
||||
public final int getPeriodCount() {
|
||||
return periods.size();
|
||||
}
|
||||
|
||||
public final Period getPeriod(int index) {
|
||||
return periods.get(index);
|
||||
}
|
||||
|
||||
public final long getPeriodDuration(int index) {
|
||||
return index == periods.size() - 1
|
||||
? (duration == -1 ? -1 : duration - periods.get(index).startMs)
|
||||
: periods.get(index + 1).startMs - periods.get(index).startMs;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -30,6 +30,7 @@ import com.google.android.exoplayer.util.Util;
|
||||
|
||||
import android.text.TextUtils;
|
||||
import android.util.Base64;
|
||||
import android.util.Pair;
|
||||
|
||||
import org.xml.sax.helpers.DefaultHandler;
|
||||
import org.xmlpull.v1.XmlPullParser;
|
||||
@ -114,16 +115,32 @@ public class MediaPresentationDescriptionParser extends DefaultHandler
|
||||
String location = null;
|
||||
|
||||
List<Period> periods = new ArrayList<>();
|
||||
long nextPeriodStartMs = dynamic ? -1 : 0;
|
||||
boolean seenEarlyAccessPeriod = false;
|
||||
do {
|
||||
xpp.next();
|
||||
if (isStartTag(xpp, "BaseURL")) {
|
||||
baseUrl = parseBaseUrl(xpp, baseUrl);
|
||||
} else if (isStartTag(xpp, "UTCTiming")) {
|
||||
utcTiming = parseUtcTiming(xpp);
|
||||
} else if (isStartTag(xpp, "Period")) {
|
||||
periods.add(parsePeriod(xpp, baseUrl, durationMs));
|
||||
} else if (isStartTag(xpp, "Location")) {
|
||||
location = xpp.nextText();
|
||||
} else if (isStartTag(xpp, "Period") && !seenEarlyAccessPeriod) {
|
||||
Pair<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"));
|
||||
|
||||
@ -149,11 +166,11 @@ public class MediaPresentationDescriptionParser extends DefaultHandler
|
||||
return new UtcTimingElement(schemeIdUri, value);
|
||||
}
|
||||
|
||||
protected Period parsePeriod(XmlPullParser xpp, String baseUrl, long mpdDurationMs)
|
||||
protected Pair<Period, Long> parsePeriod(XmlPullParser xpp, String baseUrl, long defaultStartMs)
|
||||
throws XmlPullParserException, IOException {
|
||||
String id = xpp.getAttributeValue(null, "id");
|
||||
long startMs = parseDuration(xpp, "start", 0);
|
||||
long durationMs = parseDuration(xpp, "duration", mpdDurationMs);
|
||||
long startMs = parseDuration(xpp, "start", defaultStartMs);
|
||||
long durationMs = parseDuration(xpp, "duration", -1);
|
||||
SegmentBase segmentBase = null;
|
||||
List<AdaptationSet> adaptationSets = new ArrayList<>();
|
||||
do {
|
||||
@ -161,29 +178,27 @@ public class MediaPresentationDescriptionParser extends DefaultHandler
|
||||
if (isStartTag(xpp, "BaseURL")) {
|
||||
baseUrl = parseBaseUrl(xpp, baseUrl);
|
||||
} else if (isStartTag(xpp, "AdaptationSet")) {
|
||||
adaptationSets.add(parseAdaptationSet(xpp, baseUrl, startMs, durationMs,
|
||||
segmentBase));
|
||||
adaptationSets.add(parseAdaptationSet(xpp, baseUrl, segmentBase));
|
||||
} else if (isStartTag(xpp, "SegmentBase")) {
|
||||
segmentBase = parseSegmentBase(xpp, baseUrl, null);
|
||||
} else if (isStartTag(xpp, "SegmentList")) {
|
||||
segmentBase = parseSegmentList(xpp, baseUrl, null, durationMs);
|
||||
segmentBase = parseSegmentList(xpp, baseUrl, null);
|
||||
} else if (isStartTag(xpp, "SegmentTemplate")) {
|
||||
segmentBase = parseSegmentTemplate(xpp, baseUrl, null, durationMs);
|
||||
segmentBase = parseSegmentTemplate(xpp, baseUrl, null);
|
||||
}
|
||||
} while (!isEndTag(xpp, "Period"));
|
||||
|
||||
return buildPeriod(id, startMs, durationMs, adaptationSets);
|
||||
return Pair.create(buildPeriod(id, startMs, adaptationSets), durationMs);
|
||||
}
|
||||
|
||||
protected Period buildPeriod(
|
||||
String id, long startMs, long durationMs, List<AdaptationSet> adaptationSets) {
|
||||
return new Period(id, startMs, durationMs, adaptationSets);
|
||||
protected Period buildPeriod(String id, long startMs, List<AdaptationSet> adaptationSets) {
|
||||
return new Period(id, startMs, adaptationSets);
|
||||
}
|
||||
|
||||
// AdaptationSet parsing.
|
||||
|
||||
protected AdaptationSet parseAdaptationSet(XmlPullParser xpp, String baseUrl, long periodStartMs,
|
||||
long periodDurationMs, SegmentBase segmentBase) throws XmlPullParserException, IOException {
|
||||
protected AdaptationSet parseAdaptationSet(XmlPullParser xpp, String baseUrl,
|
||||
SegmentBase segmentBase) throws XmlPullParserException, IOException {
|
||||
int id = parseInt(xpp, "id", -1);
|
||||
int contentType = parseContentType(xpp);
|
||||
|
||||
@ -208,9 +223,9 @@ public class MediaPresentationDescriptionParser extends DefaultHandler
|
||||
language = checkLanguageConsistency(language, xpp.getAttributeValue(null, "lang"));
|
||||
contentType = checkContentTypeConsistency(contentType, parseContentType(xpp));
|
||||
} else if (isStartTag(xpp, "Representation")) {
|
||||
Representation representation = parseRepresentation(xpp, baseUrl, periodStartMs,
|
||||
periodDurationMs, mimeType, codecs, width, height, frameRate, audioChannels,
|
||||
audioSamplingRate, language, segmentBase, contentProtectionsBuilder);
|
||||
Representation representation = parseRepresentation(xpp, baseUrl, mimeType, codecs, width,
|
||||
height, frameRate, audioChannels, audioSamplingRate, language, segmentBase,
|
||||
contentProtectionsBuilder);
|
||||
contentProtectionsBuilder.endRepresentation();
|
||||
contentType = checkContentTypeConsistency(contentType, getContentType(representation));
|
||||
representations.add(representation);
|
||||
@ -219,10 +234,9 @@ public class MediaPresentationDescriptionParser extends DefaultHandler
|
||||
} else if (isStartTag(xpp, "SegmentBase")) {
|
||||
segmentBase = parseSegmentBase(xpp, baseUrl, (SingleSegmentBase) segmentBase);
|
||||
} else if (isStartTag(xpp, "SegmentList")) {
|
||||
segmentBase = parseSegmentList(xpp, baseUrl, (SegmentList) segmentBase, periodDurationMs);
|
||||
segmentBase = parseSegmentList(xpp, baseUrl, (SegmentList) segmentBase);
|
||||
} else if (isStartTag(xpp, "SegmentTemplate")) {
|
||||
segmentBase = parseSegmentTemplate(xpp, baseUrl, (SegmentTemplate) segmentBase,
|
||||
periodDurationMs);
|
||||
segmentBase = parseSegmentTemplate(xpp, baseUrl, (SegmentTemplate) segmentBase);
|
||||
} else if (isStartTag(xpp)) {
|
||||
parseAdaptationSetChild(xpp);
|
||||
}
|
||||
@ -311,9 +325,8 @@ public class MediaPresentationDescriptionParser extends DefaultHandler
|
||||
// Representation parsing.
|
||||
|
||||
protected Representation parseRepresentation(XmlPullParser xpp, String baseUrl,
|
||||
long periodStartMs, long periodDurationMs, String adaptationSetMimeType,
|
||||
String adaptationSetCodecs, int adaptationSetWidth, int adaptationSetHeight,
|
||||
float adaptationSetFrameRate, int adaptationSetAudioChannels,
|
||||
String adaptationSetMimeType, String adaptationSetCodecs, int adaptationSetWidth,
|
||||
int adaptationSetHeight, float adaptationSetFrameRate, int adaptationSetAudioChannels,
|
||||
int adaptationSetAudioSamplingRate, String adaptationSetLanguage, SegmentBase segmentBase,
|
||||
ContentProtectionsBuilder contentProtectionsBuilder)
|
||||
throws XmlPullParserException, IOException {
|
||||
@ -338,10 +351,9 @@ public class MediaPresentationDescriptionParser extends DefaultHandler
|
||||
} else if (isStartTag(xpp, "SegmentBase")) {
|
||||
segmentBase = parseSegmentBase(xpp, baseUrl, (SingleSegmentBase) segmentBase);
|
||||
} else if (isStartTag(xpp, "SegmentList")) {
|
||||
segmentBase = parseSegmentList(xpp, baseUrl, (SegmentList) segmentBase, periodDurationMs);
|
||||
segmentBase = parseSegmentList(xpp, baseUrl, (SegmentList) segmentBase);
|
||||
} else if (isStartTag(xpp, "SegmentTemplate")) {
|
||||
segmentBase = parseSegmentTemplate(xpp, baseUrl, (SegmentTemplate) segmentBase,
|
||||
periodDurationMs);
|
||||
segmentBase = parseSegmentTemplate(xpp, baseUrl, (SegmentTemplate) segmentBase);
|
||||
} else if (isStartTag(xpp, "ContentProtection")) {
|
||||
contentProtectionsBuilder.addRepresentationProtection(parseContentProtection(xpp));
|
||||
}
|
||||
@ -349,7 +361,7 @@ public class MediaPresentationDescriptionParser extends DefaultHandler
|
||||
|
||||
Format format = buildFormat(id, mimeType, width, height, frameRate, audioChannels,
|
||||
audioSamplingRate, bandwidth, language, codecs);
|
||||
return buildRepresentation(periodStartMs, periodDurationMs, contentId, -1, format,
|
||||
return buildRepresentation(contentId, -1, format,
|
||||
segmentBase != null ? segmentBase : new SingleSegmentBase(baseUrl));
|
||||
}
|
||||
|
||||
@ -359,10 +371,9 @@ public class MediaPresentationDescriptionParser extends DefaultHandler
|
||||
bandwidth, language, codecs);
|
||||
}
|
||||
|
||||
protected Representation buildRepresentation(long periodStartMs, long periodDurationMs,
|
||||
String contentId, int revisionId, Format format, SegmentBase segmentBase) {
|
||||
return Representation.newInstance(periodStartMs, periodDurationMs, contentId, revisionId,
|
||||
format, segmentBase);
|
||||
protected Representation buildRepresentation(String contentId, int revisionId, Format format,
|
||||
SegmentBase segmentBase) {
|
||||
return Representation.newInstance(contentId, revisionId, format, segmentBase);
|
||||
}
|
||||
|
||||
// SegmentBase, SegmentList and SegmentTemplate parsing.
|
||||
@ -401,8 +412,8 @@ public class MediaPresentationDescriptionParser extends DefaultHandler
|
||||
indexStart, indexLength);
|
||||
}
|
||||
|
||||
protected SegmentList parseSegmentList(XmlPullParser xpp, String baseUrl, SegmentList parent,
|
||||
long periodDurationMs) throws XmlPullParserException, IOException {
|
||||
protected SegmentList parseSegmentList(XmlPullParser xpp, String baseUrl, SegmentList parent)
|
||||
throws XmlPullParserException, IOException {
|
||||
|
||||
long timescale = parseLong(xpp, "timescale", parent != null ? parent.timescale : 1);
|
||||
long presentationTimeOffset = parseLong(xpp, "presentationTimeOffset",
|
||||
@ -434,19 +445,19 @@ public class MediaPresentationDescriptionParser extends DefaultHandler
|
||||
segments = segments != null ? segments : parent.mediaSegments;
|
||||
}
|
||||
|
||||
return buildSegmentList(initialization, timescale, presentationTimeOffset, periodDurationMs,
|
||||
return buildSegmentList(initialization, timescale, presentationTimeOffset,
|
||||
startNumber, duration, timeline, segments);
|
||||
}
|
||||
|
||||
protected SegmentList buildSegmentList(RangedUri initialization, long timescale,
|
||||
long presentationTimeOffset, long periodDurationMs, int startNumber, long duration,
|
||||
long presentationTimeOffset, int startNumber, long duration,
|
||||
List<SegmentTimelineElement> timeline, List<RangedUri> segments) {
|
||||
return new SegmentList(initialization, timescale, presentationTimeOffset, periodDurationMs,
|
||||
return new SegmentList(initialization, timescale, presentationTimeOffset,
|
||||
startNumber, duration, timeline, segments);
|
||||
}
|
||||
|
||||
protected SegmentTemplate parseSegmentTemplate(XmlPullParser xpp, String baseUrl,
|
||||
SegmentTemplate parent, long periodDurationMs) throws XmlPullParserException, IOException {
|
||||
SegmentTemplate parent) throws XmlPullParserException, IOException {
|
||||
|
||||
long timescale = parseLong(xpp, "timescale", parent != null ? parent.timescale : 1);
|
||||
long presentationTimeOffset = parseLong(xpp, "presentationTimeOffset",
|
||||
@ -475,15 +486,15 @@ public class MediaPresentationDescriptionParser extends DefaultHandler
|
||||
timeline = timeline != null ? timeline : parent.segmentTimeline;
|
||||
}
|
||||
|
||||
return buildSegmentTemplate(initialization, timescale, presentationTimeOffset, periodDurationMs,
|
||||
return buildSegmentTemplate(initialization, timescale, presentationTimeOffset,
|
||||
startNumber, duration, timeline, initializationTemplate, mediaTemplate, baseUrl);
|
||||
}
|
||||
|
||||
protected SegmentTemplate buildSegmentTemplate(RangedUri initialization, long timescale,
|
||||
long presentationTimeOffset, long periodDurationMs, int startNumber, long duration,
|
||||
long presentationTimeOffset, int startNumber, long duration,
|
||||
List<SegmentTimelineElement> timeline, UrlTemplate initializationTemplate,
|
||||
UrlTemplate mediaTemplate, String baseUrl) {
|
||||
return new SegmentTemplate(initialization, timescale, presentationTimeOffset, periodDurationMs,
|
||||
return new SegmentTemplate(initialization, timescale, presentationTimeOffset,
|
||||
startNumber, duration, timeline, initializationTemplate, mediaTemplate, baseUrl);
|
||||
}
|
||||
|
||||
|
@ -33,11 +33,6 @@ public class Period {
|
||||
*/
|
||||
public final long startMs;
|
||||
|
||||
/**
|
||||
* The duration of the period in milliseconds, or -1 if the duration is unknown.
|
||||
*/
|
||||
public final long durationMs;
|
||||
|
||||
/**
|
||||
* The adaptation sets belonging to the period.
|
||||
*/
|
||||
@ -46,13 +41,11 @@ public class Period {
|
||||
/**
|
||||
* @param id The period identifier. May be null.
|
||||
* @param start The start time of the period in milliseconds.
|
||||
* @param duration The duration of the period in milliseconds, or -1 if the duration is unknown.
|
||||
* @param adaptationSets The adaptation sets belonging to the period.
|
||||
*/
|
||||
public Period(String id, long start, long duration, List<AdaptationSet> adaptationSets) {
|
||||
public Period(String id, long start, List<AdaptationSet> adaptationSets) {
|
||||
this.id = id;
|
||||
this.startMs = start;
|
||||
this.durationMs = duration;
|
||||
this.adaptationSets = Collections.unmodifiableList(adaptationSets);
|
||||
}
|
||||
|
||||
|
@ -35,7 +35,6 @@ public abstract class Representation implements FormatWrapper {
|
||||
* {@link #contentId}, which should uniquely identify that video.
|
||||
*/
|
||||
public final String contentId;
|
||||
|
||||
/**
|
||||
* Identifies the revision of the content.
|
||||
* <p>
|
||||
@ -45,22 +44,10 @@ public abstract class Representation implements FormatWrapper {
|
||||
* timestamp at which the media was encoded is often a suitable.
|
||||
*/
|
||||
public final long revisionId;
|
||||
|
||||
/**
|
||||
* The format of the representation.
|
||||
*/
|
||||
public final Format format;
|
||||
|
||||
/**
|
||||
* The start time of the enclosing period in milliseconds since the epoch.
|
||||
*/
|
||||
public final long periodStartMs;
|
||||
|
||||
/**
|
||||
* The duration of the enclosing period in milliseconds.
|
||||
*/
|
||||
public final long periodDurationMs;
|
||||
|
||||
/**
|
||||
* The offset of the presentation timestamps in the media stream relative to media time.
|
||||
*/
|
||||
@ -71,33 +58,28 @@ public abstract class Representation implements FormatWrapper {
|
||||
/**
|
||||
* Constructs a new instance.
|
||||
*
|
||||
* @param periodStartMs The start time of the enclosing period in milliseconds.
|
||||
* @param periodDurationMs The duration of the enclosing period in milliseconds, or -1 if the
|
||||
* duration is unknown.
|
||||
* @param contentId Identifies the piece of content to which this representation belongs.
|
||||
* @param revisionId Identifies the revision of the content.
|
||||
* @param format The format of the representation.
|
||||
* @param segmentBase A segment base element for the representation.
|
||||
* @return The constructed instance.
|
||||
*/
|
||||
public static Representation newInstance(long periodStartMs, long periodDurationMs,
|
||||
String contentId, long revisionId, Format format, SegmentBase segmentBase) {
|
||||
public static Representation newInstance(String contentId, long revisionId, Format format,
|
||||
SegmentBase segmentBase) {
|
||||
if (segmentBase instanceof SingleSegmentBase) {
|
||||
return new SingleSegmentRepresentation(periodStartMs, periodDurationMs, contentId, revisionId,
|
||||
format, (SingleSegmentBase) segmentBase, -1);
|
||||
return new SingleSegmentRepresentation(contentId, revisionId, format,
|
||||
(SingleSegmentBase) segmentBase, -1);
|
||||
} else if (segmentBase instanceof MultiSegmentBase) {
|
||||
return new MultiSegmentRepresentation(periodStartMs, periodDurationMs, contentId, revisionId,
|
||||
format, (MultiSegmentBase) segmentBase);
|
||||
return new MultiSegmentRepresentation(contentId, revisionId, format,
|
||||
(MultiSegmentBase) segmentBase);
|
||||
} else {
|
||||
throw new IllegalArgumentException("segmentBase must be of type SingleSegmentBase or "
|
||||
+ "MultiSegmentBase");
|
||||
}
|
||||
}
|
||||
|
||||
private Representation(long periodStartMs, long periodDurationMs, String contentId,
|
||||
long revisionId, Format format, SegmentBase segmentBase) {
|
||||
this.periodStartMs = periodStartMs;
|
||||
this.periodDurationMs = periodDurationMs;
|
||||
private Representation(String contentId, long revisionId, Format format,
|
||||
SegmentBase segmentBase) {
|
||||
this.contentId = contentId;
|
||||
this.revisionId = revisionId;
|
||||
this.format = format;
|
||||
@ -165,9 +147,6 @@ public abstract class Representation implements FormatWrapper {
|
||||
private final DashSingleSegmentIndex segmentIndex;
|
||||
|
||||
/**
|
||||
* @param periodStartMs The start time of the enclosing period in milliseconds.
|
||||
* @param periodDurationMs The duration of the enclosing period in milliseconds, or -1 if the
|
||||
* duration is unknown.
|
||||
* @param contentId Identifies the piece of content to which this representation belongs.
|
||||
* @param revisionId Identifies the revision of the content.
|
||||
* @param format The format of the representation.
|
||||
@ -178,37 +157,34 @@ public abstract class Representation implements FormatWrapper {
|
||||
* @param indexEnd The offset of the last byte of index data.
|
||||
* @param contentLength The content length, or -1 if unknown.
|
||||
*/
|
||||
public static SingleSegmentRepresentation newInstance(long periodStartMs, long periodDurationMs,
|
||||
String contentId, long revisionId, Format format, String uri, long initializationStart,
|
||||
long initializationEnd, long indexStart, long indexEnd, long contentLength) {
|
||||
public static SingleSegmentRepresentation newInstance(String contentId, long revisionId,
|
||||
Format format, String uri, long initializationStart, long initializationEnd,
|
||||
long indexStart, long indexEnd, long contentLength) {
|
||||
RangedUri rangedUri = new RangedUri(uri, null, initializationStart,
|
||||
initializationEnd - initializationStart + 1);
|
||||
SingleSegmentBase segmentBase = new SingleSegmentBase(rangedUri, 1, 0, uri, indexStart,
|
||||
indexEnd - indexStart + 1);
|
||||
return new SingleSegmentRepresentation(periodStartMs, periodDurationMs, contentId, revisionId,
|
||||
return new SingleSegmentRepresentation(contentId, revisionId,
|
||||
format, segmentBase, contentLength);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param periodStartMs The start time of the enclosing period in milliseconds.
|
||||
* @param periodDurationMs The duration of the enclosing period in milliseconds, or -1 if the
|
||||
* duration is unknown.
|
||||
* @param contentId Identifies the piece of content to which this representation belongs.
|
||||
* @param revisionId Identifies the revision of the content.
|
||||
* @param format The format of the representation.
|
||||
* @param segmentBase The segment base underlying the representation.
|
||||
* @param contentLength The content length, or -1 if unknown.
|
||||
*/
|
||||
public SingleSegmentRepresentation(long periodStartMs, long periodDurationMs, String contentId,
|
||||
long revisionId, Format format, SingleSegmentBase segmentBase, long contentLength) {
|
||||
super(periodStartMs, periodDurationMs, contentId, revisionId, format, segmentBase);
|
||||
public SingleSegmentRepresentation(String contentId, long revisionId, Format format,
|
||||
SingleSegmentBase segmentBase, long contentLength) {
|
||||
super(contentId, revisionId, format, segmentBase);
|
||||
this.uri = Uri.parse(segmentBase.uri);
|
||||
this.indexUri = segmentBase.getIndex();
|
||||
this.contentLength = contentLength;
|
||||
// If we have an index uri then the index is defined externally, and we shouldn't return one
|
||||
// directly. If we don't, then we can't do better than an index defining a single segment.
|
||||
segmentIndex = indexUri != null ? null : new DashSingleSegmentIndex(periodStartMs * 1000,
|
||||
periodDurationMs * 1000, new RangedUri(segmentBase.uri, null, 0, -1));
|
||||
segmentIndex = indexUri != null ? null
|
||||
: new DashSingleSegmentIndex(new RangedUri(segmentBase.uri, null, 0, contentLength));
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -232,17 +208,14 @@ public abstract class Representation implements FormatWrapper {
|
||||
private final MultiSegmentBase segmentBase;
|
||||
|
||||
/**
|
||||
* @param periodStartMs The start time of the enclosing period in milliseconds.
|
||||
* @param periodDurationMs The duration of the enclosing period in milliseconds, or -1 if the
|
||||
* duration is unknown.
|
||||
* @param contentId Identifies the piece of content to which this representation belongs.
|
||||
* @param revisionId Identifies the revision of the content.
|
||||
* @param format The format of the representation.
|
||||
* @param segmentBase The segment base underlying the representation.
|
||||
*/
|
||||
public MultiSegmentRepresentation(long periodStartMs, long periodDurationMs, String contentId,
|
||||
long revisionId, Format format, MultiSegmentBase segmentBase) {
|
||||
super(periodStartMs, periodDurationMs, contentId, revisionId, format, segmentBase);
|
||||
public MultiSegmentRepresentation(String contentId, long revisionId, Format format,
|
||||
MultiSegmentBase segmentBase) {
|
||||
super(contentId, revisionId, format, segmentBase);
|
||||
this.segmentBase = segmentBase;
|
||||
}
|
||||
|
||||
@ -264,18 +237,18 @@ public abstract class Representation implements FormatWrapper {
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getSegmentNum(long timeUs) {
|
||||
return segmentBase.getSegmentNum(timeUs - periodStartMs * 1000);
|
||||
public int getSegmentNum(long timeUs, long periodDurationUs) {
|
||||
return segmentBase.getSegmentNum(timeUs, periodDurationUs);
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getTimeUs(int segmentIndex) {
|
||||
return segmentBase.getSegmentTimeUs(segmentIndex) + periodStartMs * 1000;
|
||||
return segmentBase.getSegmentTimeUs(segmentIndex);
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getDurationUs(int segmentIndex) {
|
||||
return segmentBase.getSegmentDurationUs(segmentIndex);
|
||||
public long getDurationUs(int segmentIndex, long periodDurationUs) {
|
||||
return segmentBase.getSegmentDurationUs(segmentIndex, periodDurationUs);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -284,8 +257,8 @@ public abstract class Representation implements FormatWrapper {
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getLastSegmentNum() {
|
||||
return segmentBase.getLastSegmentNum();
|
||||
public int getLastSegmentNum(long periodDurationUs) {
|
||||
return segmentBase.getLastSegmentNum(periodDurationUs);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -112,7 +112,6 @@ public abstract class SegmentBase {
|
||||
*/
|
||||
public abstract static class MultiSegmentBase extends SegmentBase {
|
||||
|
||||
/* package */ final long periodDurationMs;
|
||||
/* package */ final int startNumber;
|
||||
/* package */ final long duration;
|
||||
/* package */ final List<SegmentTimelineElement> segmentTimeline;
|
||||
@ -123,7 +122,6 @@ public abstract class SegmentBase {
|
||||
* @param timescale The timescale in units per second.
|
||||
* @param presentationTimeOffset The presentation time offset. The value in seconds is the
|
||||
* division of this value and {@code timescale}.
|
||||
* @param periodDurationMs The duration of the enclosing period in milliseconds.
|
||||
* @param startNumber The sequence number of the first segment.
|
||||
* @param duration The duration of each segment in the case of fixed duration segments. The
|
||||
* value in seconds is the division of this value and {@code timescale}. If
|
||||
@ -133,22 +131,20 @@ public abstract class SegmentBase {
|
||||
* parameter.
|
||||
*/
|
||||
public MultiSegmentBase(RangedUri initialization, long timescale, long presentationTimeOffset,
|
||||
long periodDurationMs, int startNumber, long duration,
|
||||
List<SegmentTimelineElement> segmentTimeline) {
|
||||
int startNumber, long duration, List<SegmentTimelineElement> segmentTimeline) {
|
||||
super(initialization, timescale, presentationTimeOffset);
|
||||
this.periodDurationMs = periodDurationMs;
|
||||
this.startNumber = startNumber;
|
||||
this.duration = duration;
|
||||
this.segmentTimeline = segmentTimeline;
|
||||
}
|
||||
|
||||
/**
|
||||
* @see DashSegmentIndex#getSegmentNum(long)
|
||||
* @see DashSegmentIndex#getSegmentNum(long, long)
|
||||
*/
|
||||
public int getSegmentNum(long timeUs) {
|
||||
public int getSegmentNum(long timeUs, long periodDurationUs) {
|
||||
final int firstSegmentNum = getFirstSegmentNum();
|
||||
int lowIndex = firstSegmentNum;
|
||||
int highIndex = getLastSegmentNum();
|
||||
int highIndex = getLastSegmentNum(periodDurationUs);
|
||||
if (segmentTimeline == null) {
|
||||
// All segments are of equal duration (with the possible exception of the last one).
|
||||
long durationUs = (duration * C.MICROS_PER_SECOND) / timescale;
|
||||
@ -175,15 +171,15 @@ public abstract class SegmentBase {
|
||||
}
|
||||
|
||||
/**
|
||||
* @see DashSegmentIndex#getDurationUs(int)
|
||||
* @see DashSegmentIndex#getDurationUs(int, long)
|
||||
*/
|
||||
public final long getSegmentDurationUs(int sequenceNumber) {
|
||||
public final long getSegmentDurationUs(int sequenceNumber, long periodDurationUs) {
|
||||
if (segmentTimeline != null) {
|
||||
long duration = segmentTimeline.get(sequenceNumber - startNumber).duration;
|
||||
return (duration * C.MICROS_PER_SECOND) / timescale;
|
||||
} else {
|
||||
return sequenceNumber == getLastSegmentNum()
|
||||
? ((periodDurationMs * 1000) - getSegmentTimeUs(sequenceNumber))
|
||||
return sequenceNumber == getLastSegmentNum(periodDurationUs)
|
||||
? (periodDurationUs - getSegmentTimeUs(sequenceNumber))
|
||||
: ((duration * C.MICROS_PER_SECOND) / timescale);
|
||||
}
|
||||
}
|
||||
@ -218,9 +214,9 @@ public abstract class SegmentBase {
|
||||
}
|
||||
|
||||
/**
|
||||
* @see DashSegmentIndex#getLastSegmentNum()
|
||||
* @see DashSegmentIndex#getLastSegmentNum(long)
|
||||
*/
|
||||
public abstract int getLastSegmentNum();
|
||||
public abstract int getLastSegmentNum(long periodDurationUs);
|
||||
|
||||
/**
|
||||
* @see DashSegmentIndex#isExplicit()
|
||||
@ -244,7 +240,6 @@ public abstract class SegmentBase {
|
||||
* @param timescale The timescale in units per second.
|
||||
* @param presentationTimeOffset The presentation time offset. The value in seconds is the
|
||||
* division of this value and {@code timescale}.
|
||||
* @param periodDurationMs The duration of the enclosing period in milliseconds.
|
||||
* @param startNumber The sequence number of the first segment.
|
||||
* @param duration The duration of each segment in the case of fixed duration segments. The
|
||||
* value in seconds is the division of this value and {@code timescale}. If
|
||||
@ -255,10 +250,10 @@ public abstract class SegmentBase {
|
||||
* @param mediaSegments A list of {@link RangedUri}s indicating the locations of the segments.
|
||||
*/
|
||||
public SegmentList(RangedUri initialization, long timescale, long presentationTimeOffset,
|
||||
long periodDurationMs, int startNumber, long duration,
|
||||
List<SegmentTimelineElement> segmentTimeline, List<RangedUri> mediaSegments) {
|
||||
super(initialization, timescale, presentationTimeOffset, periodDurationMs, startNumber,
|
||||
duration, segmentTimeline);
|
||||
int startNumber, long duration, List<SegmentTimelineElement> segmentTimeline,
|
||||
List<RangedUri> mediaSegments) {
|
||||
super(initialization, timescale, presentationTimeOffset, startNumber, duration,
|
||||
segmentTimeline);
|
||||
this.mediaSegments = mediaSegments;
|
||||
}
|
||||
|
||||
@ -268,7 +263,7 @@ public abstract class SegmentBase {
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getLastSegmentNum() {
|
||||
public int getLastSegmentNum(long periodDurationUs) {
|
||||
return startNumber + mediaSegments.size() - 1;
|
||||
}
|
||||
|
||||
@ -296,7 +291,6 @@ public abstract class SegmentBase {
|
||||
* @param timescale The timescale in units per second.
|
||||
* @param presentationTimeOffset The presentation time offset. The value in seconds is the
|
||||
* division of this value and {@code timescale}.
|
||||
* @param periodDurationMs The duration of the enclosing period in milliseconds.
|
||||
* @param startNumber The sequence number of the first segment.
|
||||
* @param duration The duration of each segment in the case of fixed duration segments. The
|
||||
* value in seconds is the division of this value and {@code timescale}. If
|
||||
@ -311,10 +305,9 @@ public abstract class SegmentBase {
|
||||
* @param baseUrl A url to use as the base for relative urls generated by the templates.
|
||||
*/
|
||||
public SegmentTemplate(RangedUri initialization, long timescale, long presentationTimeOffset,
|
||||
long periodDurationMs, int startNumber, long duration,
|
||||
List<SegmentTimelineElement> segmentTimeline, UrlTemplate initializationTemplate,
|
||||
UrlTemplate mediaTemplate, String baseUrl) {
|
||||
super(initialization, timescale, presentationTimeOffset, periodDurationMs, startNumber,
|
||||
int startNumber, long duration, List<SegmentTimelineElement> segmentTimeline,
|
||||
UrlTemplate initializationTemplate, UrlTemplate mediaTemplate, String baseUrl) {
|
||||
super(initialization, timescale, presentationTimeOffset, startNumber,
|
||||
duration, segmentTimeline);
|
||||
this.initializationTemplate = initializationTemplate;
|
||||
this.mediaTemplate = mediaTemplate;
|
||||
@ -346,14 +339,14 @@ public abstract class SegmentBase {
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getLastSegmentNum() {
|
||||
public int getLastSegmentNum(long periodDurationUs) {
|
||||
if (segmentTimeline != null) {
|
||||
return segmentTimeline.size() + startNumber - 1;
|
||||
} else if (periodDurationMs == -1) {
|
||||
} else if (periodDurationUs == C.UNKNOWN_TIME_US) {
|
||||
return DashSegmentIndex.INDEX_UNBOUNDED;
|
||||
} else {
|
||||
long durationMs = (duration * 1000) / timescale;
|
||||
return startNumber + (int) Util.ceilDivide(periodDurationMs, durationMs) - 1;
|
||||
long durationUs = (duration * C.MICROS_PER_SECOND) / timescale;
|
||||
return startNumber + (int) Util.ceilDivide(periodDurationUs, durationUs) - 1;
|
||||
}
|
||||
}
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user