/** * For live playbacks, determines the seek position that snaps playback to be {@code * liveEdgeLatencyUs} behind the live edge of the provided manifest. * * @param manifest The manifest. * @param liveEdgeLatencyUs The live edge latency, in microseconds. * @return The seek position in microseconds. */ private static long getLiveSeekPosition( SmoothStreamingManifest manifest, long liveEdgeLatencyUs) { long liveEdgeTimestampUs = Long.MIN_VALUE; for (int i = 0; i < manifest.streamElements.length; i++) { StreamElement streamElement = manifest.streamElements[i]; if (streamElement.chunkCount > 0) { long elementLiveEdgeTimestampUs = streamElement.getStartTimeUs(streamElement.chunkCount - 1) + streamElement.getChunkDurationUs(streamElement.chunkCount - 1); liveEdgeTimestampUs = Math.max(liveEdgeTimestampUs, elementLiveEdgeTimestampUs); } } return liveEdgeTimestampUs - liveEdgeLatencyUs; }
@Override public void continueBuffering(long playbackPositionUs) { if (manifestFetcher == null || !currentManifest.isLive || fatalError != null) { return; } SmoothStreamingManifest newManifest = manifestFetcher.getManifest(); if (currentManifest != newManifest && newManifest != null) { StreamElement currentElement = currentManifest.streamElements[enabledTrack.elementIndex]; int currentElementChunkCount = currentElement.chunkCount; StreamElement newElement = newManifest.streamElements[enabledTrack.elementIndex]; if (currentElementChunkCount == 0 || newElement.chunkCount == 0) { // There's no overlap between the old and new elements because at least one is empty. currentManifestChunkOffset += currentElementChunkCount; } else { long currentElementEndTimeUs = currentElement.getStartTimeUs(currentElementChunkCount - 1) + currentElement.getChunkDurationUs(currentElementChunkCount - 1); long newElementStartTimeUs = newElement.getStartTimeUs(0); if (currentElementEndTimeUs <= newElementStartTimeUs) { // There's no overlap between the old and new elements. currentManifestChunkOffset += currentElementChunkCount; } else { // The new element overlaps with the old one. currentManifestChunkOffset += currentElement.getChunkIndex(newElementStartTimeUs); } } currentManifest = newManifest; needManifestRefresh = false; } if (needManifestRefresh && (SystemClock.elapsedRealtime() > manifestFetcher.getManifestLoadStartTimestamp() + MINIMUM_MANIFEST_REFRESH_PERIOD_MS)) { manifestFetcher.requestRefresh(); } }
@Override public final void getChunkOperation( List<? extends MediaChunk> queue, long playbackPositionUs, ChunkOperationHolder out) { if (fatalError != null) { out.chunk = null; return; } evaluation.queueSize = queue.size(); if (enabledTrack.isAdaptive()) { adaptiveFormatEvaluator.evaluate( queue, playbackPositionUs, enabledTrack.adaptiveFormats, evaluation); } else { evaluation.format = enabledTrack.fixedFormat; evaluation.trigger = Chunk.TRIGGER_MANUAL; } Format selectedFormat = evaluation.format; out.queueSize = evaluation.queueSize; if (selectedFormat == null) { out.chunk = null; return; } else if (out.queueSize == queue.size() && out.chunk != null && out.chunk.format.equals(selectedFormat)) { // We already have a chunk, and the evaluation hasn't changed either the format or the size // of the queue. Leave unchanged. return; } // In all cases where we return before instantiating a new chunk, we want out.chunk to be null. out.chunk = null; StreamElement streamElement = currentManifest.streamElements[enabledTrack.elementIndex]; if (streamElement.chunkCount == 0) { if (currentManifest.isLive) { needManifestRefresh = true; } else { out.endOfStream = true; } return; } int chunkIndex; if (queue.isEmpty()) { if (live) { playbackPositionUs = getLiveSeekPosition(currentManifest, liveEdgeLatencyUs); } chunkIndex = streamElement.getChunkIndex(playbackPositionUs); } else { MediaChunk previous = queue.get(out.queueSize - 1); chunkIndex = previous.chunkIndex + 1 - currentManifestChunkOffset; } if (live && chunkIndex < 0) { // This is before the first chunk in the current manifest. fatalError = new BehindLiveWindowException(); return; } else if (currentManifest.isLive) { if (chunkIndex >= streamElement.chunkCount) { // This is beyond the last chunk in the current manifest. needManifestRefresh = true; return; } else if (chunkIndex == streamElement.chunkCount - 1) { // This is the last chunk in the current manifest. Mark the manifest as being finished, // but continue to return the final chunk. needManifestRefresh = true; } } else if (chunkIndex >= streamElement.chunkCount) { out.endOfStream = true; return; } boolean isLastChunk = !currentManifest.isLive && chunkIndex == streamElement.chunkCount - 1; long chunkStartTimeUs = streamElement.getStartTimeUs(chunkIndex); long chunkEndTimeUs = isLastChunk ? -1 : chunkStartTimeUs + streamElement.getChunkDurationUs(chunkIndex); int currentAbsoluteChunkIndex = chunkIndex + currentManifestChunkOffset; int manifestTrackIndex = getManifestTrackIndex(streamElement, selectedFormat); int manifestTrackKey = getManifestTrackKey(enabledTrack.elementIndex, manifestTrackIndex); Uri uri = streamElement.buildRequestUri(manifestTrackIndex, chunkIndex); Chunk mediaChunk = newMediaChunk( selectedFormat, uri, null, extractorWrappers.get(manifestTrackKey), drmInitData, dataSource, currentAbsoluteChunkIndex, chunkStartTimeUs, chunkEndTimeUs, evaluation.trigger, mediaFormats.get(manifestTrackKey), enabledTrack.adaptiveMaxWidth, enabledTrack.adaptiveMaxHeight); out.chunk = mediaChunk; }