@Override public final void getChunkOperation( List<? extends MediaChunk> queue, long seekPositionUs, 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) { seekPositionUs = getLiveSeekPosition(currentManifest, liveEdgeLatencyUs); } chunkIndex = streamElement.getChunkIndex(seekPositionUs); } 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; }
/** * Updates the provided {@link ChunkOperationHolder} to contain the next operation that should be * performed by the calling {@link HlsSampleSource}. * * @param previousTsChunk The previously loaded chunk that the next chunk should follow. * @param playbackPositionUs The current playback position. If previousTsChunk is null then this * parameter is the position from which playback is expected to start (or restart) and hence * should be interpreted as a seek position. * @param out The holder to populate with the result. {@link ChunkOperationHolder#queueSize} is * unused. */ public void getChunkOperation( TsChunk previousTsChunk, long seekPositionUs, long playbackPositionUs, ChunkOperationHolder out) { int nextVariantIndex; boolean switchingVariantSpliced; if (adaptiveMode == ADAPTIVE_MODE_NONE) { nextVariantIndex = selectedVariantIndex; switchingVariantSpliced = false; } else { nextVariantIndex = getNextVariantIndex(previousTsChunk, playbackPositionUs); switchingVariantSpliced = previousTsChunk != null && !variants[nextVariantIndex].format.equals(previousTsChunk.format) && adaptiveMode == ADAPTIVE_MODE_SPLICE; } HlsMediaPlaylist mediaPlaylist = variantPlaylists[nextVariantIndex]; if (mediaPlaylist == null) { // We don't have the media playlist for the next variant. Request it now. out.chunk = newMediaPlaylistChunk(nextVariantIndex); return; } selectedVariantIndex = nextVariantIndex; int chunkMediaSequence = 0; if (live) { if (previousTsChunk == null) { if (seekPositionUs == 0) { chunkMediaSequence = getLiveStartChunkMediaSequence(nextVariantIndex); } else { chunkMediaSequence = Util.binarySearchFloor(mediaPlaylist.segments, seekPositionUs, true, true) + mediaPlaylist.mediaSequence; } } else { chunkMediaSequence = switchingVariantSpliced ? previousTsChunk.chunkIndex : previousTsChunk.chunkIndex + 1; if (chunkMediaSequence < mediaPlaylist.mediaSequence) { fatalError = new BehindLiveWindowException(); return; } } } else { // Not live. if (previousTsChunk == null) { chunkMediaSequence = Util.binarySearchFloor(mediaPlaylist.segments, playbackPositionUs, true, true) + mediaPlaylist.mediaSequence; } else { chunkMediaSequence = switchingVariantSpliced ? previousTsChunk.chunkIndex : previousTsChunk.chunkIndex + 1; } } int chunkIndex = chunkMediaSequence - mediaPlaylist.mediaSequence; if (chunkIndex >= mediaPlaylist.segments.size()) { if (!mediaPlaylist.live) { out.endOfStream = true; } else if (shouldRerequestLiveMediaPlaylist(nextVariantIndex)) { out.chunk = newMediaPlaylistChunk(nextVariantIndex); } return; } HlsMediaPlaylist.Segment segment = mediaPlaylist.segments.get(chunkIndex); Uri chunkUri = UriUtil.resolveToUri(mediaPlaylist.baseUri, segment.url); // Check if encryption is specified. if (segment.isEncrypted) { Uri keyUri = UriUtil.resolveToUri(mediaPlaylist.baseUri, segment.encryptionKeyUri); if (!keyUri.equals(encryptionKeyUri)) { // Encryption is specified and the key has changed. out.chunk = newEncryptionKeyChunk(keyUri, segment.encryptionIV, selectedVariantIndex); return; } if (!Util.areEqual(segment.encryptionIV, encryptionIvString)) { setEncryptionData(keyUri, segment.encryptionIV, encryptionKey); } } else { clearEncryptionData(); } // Configure the data source and spec for the chunk. DataSpec dataSpec = new DataSpec(chunkUri, segment.byterangeOffset, segment.byterangeLength, null); // Compute start and end times, and the sequence number of the next chunk. long startTimeUs; if (live) { if (previousTsChunk == null) { startTimeUs = segment.startTimeUs; } else if (switchingVariantSpliced) { startTimeUs = previousTsChunk.startTimeUs; } else { startTimeUs = previousTsChunk.endTimeUs; } } else /* Not live */ { startTimeUs = segment.startTimeUs; } long endTimeUs = startTimeUs + (long) (segment.durationSecs * C.MICROS_PER_SECOND); int trigger = Chunk.TRIGGER_UNSPECIFIED; Format format = variants[selectedVariantIndex].format; // Configure the extractor that will read the chunk. HlsExtractorWrapper extractorWrapper; String lastPathSegment = chunkUri.getLastPathSegment(); if (lastPathSegment.endsWith(AAC_FILE_EXTENSION)) { // TODO: Inject a timestamp adjuster and use it along with ID3 PRIV tag values with owner // identifier com.apple.streaming.transportStreamTimestamp. This may also apply to the MP3 // case below. Extractor extractor = new AdtsExtractor(startTimeUs); extractorWrapper = new HlsExtractorWrapper( trigger, format, startTimeUs, extractor, switchingVariantSpliced, MediaFormat.NO_VALUE, MediaFormat.NO_VALUE); } else if (lastPathSegment.endsWith(MP3_FILE_EXTENSION)) { Extractor extractor = new Mp3Extractor(startTimeUs); extractorWrapper = new HlsExtractorWrapper( trigger, format, startTimeUs, extractor, switchingVariantSpliced, MediaFormat.NO_VALUE, MediaFormat.NO_VALUE); } else if (lastPathSegment.endsWith(WEBVTT_FILE_EXTENSION) || lastPathSegment.endsWith(VTT_FILE_EXTENSION)) { PtsTimestampAdjuster timestampAdjuster = timestampAdjusterProvider.getAdjuster( isMaster, segment.discontinuitySequenceNumber, startTimeUs); if (timestampAdjuster == null) { // The master source has yet to instantiate an adjuster for the discontinuity sequence. // TODO: There's probably an edge case if the master starts playback at a chunk belonging to // a discontinuity sequence greater than the one that this source is trying to start at. return; } Extractor extractor = new WebvttExtractor(timestampAdjuster); extractorWrapper = new HlsExtractorWrapper( trigger, format, startTimeUs, extractor, switchingVariantSpliced, MediaFormat.NO_VALUE, MediaFormat.NO_VALUE); } else if (previousTsChunk == null || previousTsChunk.discontinuitySequenceNumber != segment.discontinuitySequenceNumber || !format.equals(previousTsChunk.format)) { // MPEG-2 TS segments, but we need a new extractor. PtsTimestampAdjuster timestampAdjuster = timestampAdjusterProvider.getAdjuster( isMaster, segment.discontinuitySequenceNumber, startTimeUs); if (timestampAdjuster == null) { // The master source has yet to instantiate an adjuster for the discontinuity sequence. return; } int workaroundFlags = 0; String codecs = format.codecs; if (!TextUtils.isEmpty(codecs)) { // Sometimes AAC and H264 streams are declared in TS chunks even though they don't really // exist. If we know from the codec attribute that they don't exist, then we can explicitly // ignore them even if they're declared. if (MimeTypes.getAudioMediaMimeType(codecs) != MimeTypes.AUDIO_AAC) { workaroundFlags |= TsExtractor.WORKAROUND_IGNORE_AAC_STREAM; } if (MimeTypes.getVideoMediaMimeType(codecs) != MimeTypes.VIDEO_H264) { workaroundFlags |= TsExtractor.WORKAROUND_IGNORE_H264_STREAM; } } Extractor extractor = new TsExtractor(timestampAdjuster, workaroundFlags); ExposedTrack selectedTrack = tracks.get(selectedTrackIndex); extractorWrapper = new HlsExtractorWrapper( trigger, format, startTimeUs, extractor, switchingVariantSpliced, selectedTrack.adaptiveMaxWidth, selectedTrack.adaptiveMaxHeight); } else { // MPEG-2 TS segments, and we need to continue using the same extractor. extractorWrapper = previousTsChunk.extractorWrapper; } out.chunk = new TsChunk( dataSource, dataSpec, trigger, format, startTimeUs, endTimeUs, chunkMediaSequence, segment.discontinuitySequenceNumber, extractorWrapper, encryptionKey, encryptionIv); }