use of com.google.android.exoplayer2.source.hls.playlist.HlsMediaPlaylist.Part in project ExoPlayer by google.
the class DefaultAnalyticsCollectorTest method release_withCallbacksArrivingAfterRelease_onPlayerReleasedForwardedLast.
@Test
public void release_withCallbacksArrivingAfterRelease_onPlayerReleasedForwardedLast() throws Exception {
FakeClock fakeClock = new FakeClock(/* initialTimeMs= */
0, /* isAutoAdvancing= */
true);
ExoPlayer exoPlayer = new TestExoPlayerBuilder(ApplicationProvider.getApplicationContext()).setClock(fakeClock).build();
AnalyticsListener analyticsListener = spy(new AnalyticsListener() {
@Override
public void onVideoDisabled(EventTime eventTime, DecoderCounters decoderCounters) {
// Add delay in callback to test whether event timestamp and release timestamp are
// in the correct order.
fakeClock.advanceTime(1);
}
});
exoPlayer.addAnalyticsListener(analyticsListener);
// Prepare with media to ensure video renderer is enabled.
exoPlayer.setMediaSource(new FakeMediaSource(new FakeTimeline(), ExoPlayerTestRunner.VIDEO_FORMAT));
exoPlayer.prepare();
TestPlayerRunHelper.runUntilPlaybackState(exoPlayer, Player.STATE_READY);
// Release and add delay on releasing thread to verify timestamps of events.
exoPlayer.release();
long releaseTimeMs = fakeClock.currentTimeMillis();
fakeClock.advanceTime(1);
ShadowLooper.idleMainLooper();
// Verify video disable events and release events arrived in order.
ArgumentCaptor<AnalyticsListener.EventTime> videoDisabledEventTime = ArgumentCaptor.forClass(AnalyticsListener.EventTime.class);
ArgumentCaptor<AnalyticsListener.EventTime> releasedEventTime = ArgumentCaptor.forClass(AnalyticsListener.EventTime.class);
InOrder inOrder = inOrder(analyticsListener);
inOrder.verify(analyticsListener).onVideoDisabled(videoDisabledEventTime.capture(), any());
inOrder.verify(analyticsListener).onEvents(same(exoPlayer), argThat(events -> events.contains(EVENT_VIDEO_DISABLED)));
inOrder.verify(analyticsListener).onPlayerReleased(releasedEventTime.capture());
inOrder.verify(analyticsListener).onEvents(same(exoPlayer), argThat(events -> events.contains(EVENT_PLAYER_RELEASED)));
// Verify order of timestamps of these events.
// This verification is needed as a regression test against [internal ref: b/195396384]. The
// root cause of the regression was an onPlayerReleased timestamp that was less than the
// previously reported timestamps for other events triggered as part of the release.
long videoDisableTimeMs = videoDisabledEventTime.getValue().realtimeMs;
assertThat(videoDisableTimeMs).isGreaterThan(releaseTimeMs);
assertThat(releasedEventTime.getValue().realtimeMs).isGreaterThan(videoDisableTimeMs);
}
use of com.google.android.exoplayer2.source.hls.playlist.HlsMediaPlaylist.Part in project ExoPlayer by google.
the class HlsPlaylistParser method parseMediaPlaylist.
private static HlsMediaPlaylist parseMediaPlaylist(HlsMultivariantPlaylist multivariantPlaylist, @Nullable HlsMediaPlaylist previousMediaPlaylist, LineIterator iterator, String baseUri) throws IOException {
@HlsMediaPlaylist.PlaylistType int playlistType = HlsMediaPlaylist.PLAYLIST_TYPE_UNKNOWN;
long startOffsetUs = C.TIME_UNSET;
long mediaSequence = 0;
// Default version == 1.
int version = 1;
long targetDurationUs = C.TIME_UNSET;
long partTargetDurationUs = C.TIME_UNSET;
boolean hasIndependentSegmentsTag = multivariantPlaylist.hasIndependentSegments;
boolean hasEndTag = false;
@Nullable Segment initializationSegment = null;
HashMap<String, String> variableDefinitions = new HashMap<>();
HashMap<String, Segment> urlToInferredInitSegment = new HashMap<>();
List<Segment> segments = new ArrayList<>();
List<Part> trailingParts = new ArrayList<>();
@Nullable Part preloadPart = null;
List<RenditionReport> renditionReports = new ArrayList<>();
List<String> tags = new ArrayList<>();
long segmentDurationUs = 0;
String segmentTitle = "";
boolean hasDiscontinuitySequence = false;
int playlistDiscontinuitySequence = 0;
int relativeDiscontinuitySequence = 0;
long playlistStartTimeUs = 0;
long segmentStartTimeUs = 0;
boolean preciseStart = false;
long segmentByteRangeOffset = 0;
long segmentByteRangeLength = C.LENGTH_UNSET;
long partStartTimeUs = 0;
long partByteRangeOffset = 0;
boolean isIFrameOnly = false;
long segmentMediaSequence = 0;
boolean hasGapTag = false;
HlsMediaPlaylist.ServerControl serverControl = new HlsMediaPlaylist.ServerControl(/* skipUntilUs= */
C.TIME_UNSET, /* canSkipDateRanges= */
false, /* holdBackUs= */
C.TIME_UNSET, /* partHoldBackUs= */
C.TIME_UNSET, /* canBlockReload= */
false);
@Nullable DrmInitData playlistProtectionSchemes = null;
@Nullable String fullSegmentEncryptionKeyUri = null;
@Nullable String fullSegmentEncryptionIV = null;
TreeMap<String, SchemeData> currentSchemeDatas = new TreeMap<>();
@Nullable String encryptionScheme = null;
@Nullable DrmInitData cachedDrmInitData = null;
String line;
while (iterator.hasNext()) {
line = iterator.next();
if (line.startsWith(TAG_PREFIX)) {
// We expose all tags through the playlist.
tags.add(line);
}
if (line.startsWith(TAG_PLAYLIST_TYPE)) {
String playlistTypeString = parseStringAttr(line, REGEX_PLAYLIST_TYPE, variableDefinitions);
if ("VOD".equals(playlistTypeString)) {
playlistType = HlsMediaPlaylist.PLAYLIST_TYPE_VOD;
} else if ("EVENT".equals(playlistTypeString)) {
playlistType = HlsMediaPlaylist.PLAYLIST_TYPE_EVENT;
}
} else if (line.equals(TAG_IFRAME)) {
isIFrameOnly = true;
} else if (line.startsWith(TAG_START)) {
startOffsetUs = (long) (parseDoubleAttr(line, REGEX_TIME_OFFSET) * C.MICROS_PER_SECOND);
preciseStart = parseOptionalBooleanAttribute(line, REGEX_PRECISE, /* defaultValue= */
false);
} else if (line.startsWith(TAG_SERVER_CONTROL)) {
serverControl = parseServerControl(line);
} else if (line.startsWith(TAG_PART_INF)) {
double partTargetDurationSeconds = parseDoubleAttr(line, REGEX_PART_TARGET_DURATION);
partTargetDurationUs = (long) (partTargetDurationSeconds * C.MICROS_PER_SECOND);
} else if (line.startsWith(TAG_INIT_SEGMENT)) {
String uri = parseStringAttr(line, REGEX_URI, variableDefinitions);
String byteRange = parseOptionalStringAttr(line, REGEX_ATTR_BYTERANGE, variableDefinitions);
if (byteRange != null) {
String[] splitByteRange = Util.split(byteRange, "@");
segmentByteRangeLength = Long.parseLong(splitByteRange[0]);
if (splitByteRange.length > 1) {
segmentByteRangeOffset = Long.parseLong(splitByteRange[1]);
}
}
if (segmentByteRangeLength == C.LENGTH_UNSET) {
// The segment has no byte range defined.
segmentByteRangeOffset = 0;
}
if (fullSegmentEncryptionKeyUri != null && fullSegmentEncryptionIV == null) {
// See RFC 8216, Section 4.3.2.5.
throw ParserException.createForMalformedManifest("The encryption IV attribute must be present when an initialization segment is" + " encrypted with METHOD=AES-128.", /* cause= */
null);
}
initializationSegment = new Segment(uri, segmentByteRangeOffset, segmentByteRangeLength, fullSegmentEncryptionKeyUri, fullSegmentEncryptionIV);
if (segmentByteRangeLength != C.LENGTH_UNSET) {
segmentByteRangeOffset += segmentByteRangeLength;
}
segmentByteRangeLength = C.LENGTH_UNSET;
} else if (line.startsWith(TAG_TARGET_DURATION)) {
targetDurationUs = parseIntAttr(line, REGEX_TARGET_DURATION) * C.MICROS_PER_SECOND;
} else if (line.startsWith(TAG_MEDIA_SEQUENCE)) {
mediaSequence = parseLongAttr(line, REGEX_MEDIA_SEQUENCE);
segmentMediaSequence = mediaSequence;
} else if (line.startsWith(TAG_VERSION)) {
version = parseIntAttr(line, REGEX_VERSION);
} else if (line.startsWith(TAG_DEFINE)) {
String importName = parseOptionalStringAttr(line, REGEX_IMPORT, variableDefinitions);
if (importName != null) {
String value = multivariantPlaylist.variableDefinitions.get(importName);
if (value != null) {
variableDefinitions.put(importName, value);
} else {
// The multivariant playlist does not declare the imported variable. Ignore.
}
} else {
variableDefinitions.put(parseStringAttr(line, REGEX_NAME, variableDefinitions), parseStringAttr(line, REGEX_VALUE, variableDefinitions));
}
} else if (line.startsWith(TAG_MEDIA_DURATION)) {
segmentDurationUs = parseTimeSecondsToUs(line, REGEX_MEDIA_DURATION);
segmentTitle = parseOptionalStringAttr(line, REGEX_MEDIA_TITLE, "", variableDefinitions);
} else if (line.startsWith(TAG_SKIP)) {
int skippedSegmentCount = parseIntAttr(line, REGEX_SKIPPED_SEGMENTS);
checkState(previousMediaPlaylist != null && segments.isEmpty());
int startIndex = (int) (mediaSequence - castNonNull(previousMediaPlaylist).mediaSequence);
int endIndex = startIndex + skippedSegmentCount;
if (startIndex < 0 || endIndex > previousMediaPlaylist.segments.size()) {
// Throw to force a reload if not all segments are available in the previous playlist.
throw new DeltaUpdateException();
}
for (int i = startIndex; i < endIndex; i++) {
Segment segment = previousMediaPlaylist.segments.get(i);
if (mediaSequence != previousMediaPlaylist.mediaSequence) {
// If the media sequences of the playlists are not the same, we need to recreate the
// object with the updated relative start time and the relative discontinuity
// sequence. With identical playlist media sequences these values do not change.
int newRelativeDiscontinuitySequence = previousMediaPlaylist.discontinuitySequence - playlistDiscontinuitySequence + segment.relativeDiscontinuitySequence;
segment = segment.copyWith(segmentStartTimeUs, newRelativeDiscontinuitySequence);
}
segments.add(segment);
segmentStartTimeUs += segment.durationUs;
partStartTimeUs = segmentStartTimeUs;
if (segment.byteRangeLength != C.LENGTH_UNSET) {
segmentByteRangeOffset = segment.byteRangeOffset + segment.byteRangeLength;
}
relativeDiscontinuitySequence = segment.relativeDiscontinuitySequence;
initializationSegment = segment.initializationSegment;
cachedDrmInitData = segment.drmInitData;
fullSegmentEncryptionKeyUri = segment.fullSegmentEncryptionKeyUri;
if (segment.encryptionIV == null || !segment.encryptionIV.equals(Long.toHexString(segmentMediaSequence))) {
fullSegmentEncryptionIV = segment.encryptionIV;
}
segmentMediaSequence++;
}
} else if (line.startsWith(TAG_KEY)) {
String method = parseStringAttr(line, REGEX_METHOD, variableDefinitions);
String keyFormat = parseOptionalStringAttr(line, REGEX_KEYFORMAT, KEYFORMAT_IDENTITY, variableDefinitions);
fullSegmentEncryptionKeyUri = null;
fullSegmentEncryptionIV = null;
if (METHOD_NONE.equals(method)) {
currentSchemeDatas.clear();
cachedDrmInitData = null;
} else /* !METHOD_NONE.equals(method) */
{
fullSegmentEncryptionIV = parseOptionalStringAttr(line, REGEX_IV, variableDefinitions);
if (KEYFORMAT_IDENTITY.equals(keyFormat)) {
if (METHOD_AES_128.equals(method)) {
// The segment is fully encrypted using an identity key.
fullSegmentEncryptionKeyUri = parseStringAttr(line, REGEX_URI, variableDefinitions);
} else {
// Do nothing. Samples are encrypted using an identity key, but this is not supported.
// Hopefully, a traditional DRM alternative is also provided.
}
} else {
if (encryptionScheme == null) {
encryptionScheme = parseEncryptionScheme(method);
}
SchemeData schemeData = parseDrmSchemeData(line, keyFormat, variableDefinitions);
if (schemeData != null) {
cachedDrmInitData = null;
currentSchemeDatas.put(keyFormat, schemeData);
}
}
}
} else if (line.startsWith(TAG_BYTERANGE)) {
String byteRange = parseStringAttr(line, REGEX_BYTERANGE, variableDefinitions);
String[] splitByteRange = Util.split(byteRange, "@");
segmentByteRangeLength = Long.parseLong(splitByteRange[0]);
if (splitByteRange.length > 1) {
segmentByteRangeOffset = Long.parseLong(splitByteRange[1]);
}
} else if (line.startsWith(TAG_DISCONTINUITY_SEQUENCE)) {
hasDiscontinuitySequence = true;
playlistDiscontinuitySequence = Integer.parseInt(line.substring(line.indexOf(':') + 1));
} else if (line.equals(TAG_DISCONTINUITY)) {
relativeDiscontinuitySequence++;
} else if (line.startsWith(TAG_PROGRAM_DATE_TIME)) {
if (playlistStartTimeUs == 0) {
long programDatetimeUs = Util.msToUs(Util.parseXsDateTime(line.substring(line.indexOf(':') + 1)));
playlistStartTimeUs = programDatetimeUs - segmentStartTimeUs;
}
} else if (line.equals(TAG_GAP)) {
hasGapTag = true;
} else if (line.equals(TAG_INDEPENDENT_SEGMENTS)) {
hasIndependentSegmentsTag = true;
} else if (line.equals(TAG_ENDLIST)) {
hasEndTag = true;
} else if (line.startsWith(TAG_RENDITION_REPORT)) {
long lastMediaSequence = parseOptionalLongAttr(line, REGEX_LAST_MSN, C.INDEX_UNSET);
int lastPartIndex = parseOptionalIntAttr(line, REGEX_LAST_PART, C.INDEX_UNSET);
String uri = parseStringAttr(line, REGEX_URI, variableDefinitions);
Uri playlistUri = Uri.parse(UriUtil.resolve(baseUri, uri));
renditionReports.add(new RenditionReport(playlistUri, lastMediaSequence, lastPartIndex));
} else if (line.startsWith(TAG_PRELOAD_HINT)) {
if (preloadPart != null) {
continue;
}
String type = parseStringAttr(line, REGEX_PRELOAD_HINT_TYPE, variableDefinitions);
if (!TYPE_PART.equals(type)) {
continue;
}
String url = parseStringAttr(line, REGEX_URI, variableDefinitions);
long byteRangeStart = parseOptionalLongAttr(line, REGEX_BYTERANGE_START, /* defaultValue= */
C.LENGTH_UNSET);
long byteRangeLength = parseOptionalLongAttr(line, REGEX_BYTERANGE_LENGTH, /* defaultValue= */
C.LENGTH_UNSET);
@Nullable String segmentEncryptionIV = getSegmentEncryptionIV(segmentMediaSequence, fullSegmentEncryptionKeyUri, fullSegmentEncryptionIV);
if (cachedDrmInitData == null && !currentSchemeDatas.isEmpty()) {
SchemeData[] schemeDatas = currentSchemeDatas.values().toArray(new SchemeData[0]);
cachedDrmInitData = new DrmInitData(encryptionScheme, schemeDatas);
if (playlistProtectionSchemes == null) {
playlistProtectionSchemes = getPlaylistProtectionSchemes(encryptionScheme, schemeDatas);
}
}
if (byteRangeStart == C.LENGTH_UNSET || byteRangeLength != C.LENGTH_UNSET) {
// Skip preload part if it is an unbounded range request.
preloadPart = new Part(url, initializationSegment, /* durationUs= */
0, relativeDiscontinuitySequence, partStartTimeUs, cachedDrmInitData, fullSegmentEncryptionKeyUri, segmentEncryptionIV, byteRangeStart != C.LENGTH_UNSET ? byteRangeStart : 0, byteRangeLength, /* hasGapTag= */
false, /* isIndependent= */
false, /* isPreload= */
true);
}
} else if (line.startsWith(TAG_PART)) {
@Nullable String segmentEncryptionIV = getSegmentEncryptionIV(segmentMediaSequence, fullSegmentEncryptionKeyUri, fullSegmentEncryptionIV);
String url = parseStringAttr(line, REGEX_URI, variableDefinitions);
long partDurationUs = (long) (parseDoubleAttr(line, REGEX_ATTR_DURATION) * C.MICROS_PER_SECOND);
boolean isIndependent = parseOptionalBooleanAttribute(line, REGEX_INDEPENDENT, /* defaultValue= */
false);
// The first part of a segment is always independent if the segments are independent.
isIndependent |= hasIndependentSegmentsTag && trailingParts.isEmpty();
boolean isGap = parseOptionalBooleanAttribute(line, REGEX_GAP, /* defaultValue= */
false);
@Nullable String byteRange = parseOptionalStringAttr(line, REGEX_ATTR_BYTERANGE, variableDefinitions);
long partByteRangeLength = C.LENGTH_UNSET;
if (byteRange != null) {
String[] splitByteRange = Util.split(byteRange, "@");
partByteRangeLength = Long.parseLong(splitByteRange[0]);
if (splitByteRange.length > 1) {
partByteRangeOffset = Long.parseLong(splitByteRange[1]);
}
}
if (partByteRangeLength == C.LENGTH_UNSET) {
partByteRangeOffset = 0;
}
if (cachedDrmInitData == null && !currentSchemeDatas.isEmpty()) {
SchemeData[] schemeDatas = currentSchemeDatas.values().toArray(new SchemeData[0]);
cachedDrmInitData = new DrmInitData(encryptionScheme, schemeDatas);
if (playlistProtectionSchemes == null) {
playlistProtectionSchemes = getPlaylistProtectionSchemes(encryptionScheme, schemeDatas);
}
}
trailingParts.add(new Part(url, initializationSegment, partDurationUs, relativeDiscontinuitySequence, partStartTimeUs, cachedDrmInitData, fullSegmentEncryptionKeyUri, segmentEncryptionIV, partByteRangeOffset, partByteRangeLength, isGap, isIndependent, /* isPreload= */
false));
partStartTimeUs += partDurationUs;
if (partByteRangeLength != C.LENGTH_UNSET) {
partByteRangeOffset += partByteRangeLength;
}
} else if (!line.startsWith("#")) {
@Nullable String segmentEncryptionIV = getSegmentEncryptionIV(segmentMediaSequence, fullSegmentEncryptionKeyUri, fullSegmentEncryptionIV);
segmentMediaSequence++;
String segmentUri = replaceVariableReferences(line, variableDefinitions);
@Nullable Segment inferredInitSegment = urlToInferredInitSegment.get(segmentUri);
if (segmentByteRangeLength == C.LENGTH_UNSET) {
// The segment has no byte range defined.
segmentByteRangeOffset = 0;
} else if (isIFrameOnly && initializationSegment == null && inferredInitSegment == null) {
// The segment is a resource byte range without an initialization segment.
// As per RFC 8216, Section 4.3.3.6, we assume the initialization section exists in the
// bytes preceding the first segment in this segment's URL.
// We assume the implicit initialization segment is unencrypted, since there's no way for
// the playlist to provide an initialization vector for it.
inferredInitSegment = new Segment(segmentUri, /* byteRangeOffset= */
0, segmentByteRangeOffset, /* fullSegmentEncryptionKeyUri= */
null, /* encryptionIV= */
null);
urlToInferredInitSegment.put(segmentUri, inferredInitSegment);
}
if (cachedDrmInitData == null && !currentSchemeDatas.isEmpty()) {
SchemeData[] schemeDatas = currentSchemeDatas.values().toArray(new SchemeData[0]);
cachedDrmInitData = new DrmInitData(encryptionScheme, schemeDatas);
if (playlistProtectionSchemes == null) {
playlistProtectionSchemes = getPlaylistProtectionSchemes(encryptionScheme, schemeDatas);
}
}
segments.add(new Segment(segmentUri, initializationSegment != null ? initializationSegment : inferredInitSegment, segmentTitle, segmentDurationUs, relativeDiscontinuitySequence, segmentStartTimeUs, cachedDrmInitData, fullSegmentEncryptionKeyUri, segmentEncryptionIV, segmentByteRangeOffset, segmentByteRangeLength, hasGapTag, trailingParts));
segmentStartTimeUs += segmentDurationUs;
partStartTimeUs = segmentStartTimeUs;
segmentDurationUs = 0;
segmentTitle = "";
trailingParts = new ArrayList<>();
if (segmentByteRangeLength != C.LENGTH_UNSET) {
segmentByteRangeOffset += segmentByteRangeLength;
}
segmentByteRangeLength = C.LENGTH_UNSET;
hasGapTag = false;
}
}
Map<Uri, RenditionReport> renditionReportMap = new HashMap<>();
for (int i = 0; i < renditionReports.size(); i++) {
RenditionReport renditionReport = renditionReports.get(i);
long lastMediaSequence = renditionReport.lastMediaSequence;
if (lastMediaSequence == C.INDEX_UNSET) {
lastMediaSequence = mediaSequence + segments.size() - (trailingParts.isEmpty() ? 1 : 0);
}
int lastPartIndex = renditionReport.lastPartIndex;
if (lastPartIndex == C.INDEX_UNSET && partTargetDurationUs != C.TIME_UNSET) {
List<Part> lastParts = trailingParts.isEmpty() ? Iterables.getLast(segments).parts : trailingParts;
lastPartIndex = lastParts.size() - 1;
}
renditionReportMap.put(renditionReport.playlistUri, new RenditionReport(renditionReport.playlistUri, lastMediaSequence, lastPartIndex));
}
if (preloadPart != null) {
trailingParts.add(preloadPart);
}
return new HlsMediaPlaylist(playlistType, baseUri, tags, startOffsetUs, preciseStart, playlistStartTimeUs, hasDiscontinuitySequence, playlistDiscontinuitySequence, mediaSequence, version, targetDurationUs, partTargetDurationUs, hasIndependentSegmentsTag, hasEndTag, /* hasProgramDateTime= */
playlistStartTimeUs != 0, playlistProtectionSchemes, segments, trailingParts, serverControl, renditionReportMap);
}
use of com.google.android.exoplayer2.source.hls.playlist.HlsMediaPlaylist.Part in project ExoPlayer by google.
the class HlsChunkSource method getNextSegmentHolder.
@Nullable
private static SegmentBaseHolder getNextSegmentHolder(HlsMediaPlaylist mediaPlaylist, long nextMediaSequence, int nextPartIndex) {
int segmentIndexInPlaylist = (int) (nextMediaSequence - mediaPlaylist.mediaSequence);
if (segmentIndexInPlaylist == mediaPlaylist.segments.size()) {
int index = nextPartIndex != C.INDEX_UNSET ? nextPartIndex : 0;
return index < mediaPlaylist.trailingParts.size() ? new SegmentBaseHolder(mediaPlaylist.trailingParts.get(index), nextMediaSequence, index) : null;
}
Segment mediaSegment = mediaPlaylist.segments.get(segmentIndexInPlaylist);
if (nextPartIndex == C.INDEX_UNSET) {
return new SegmentBaseHolder(mediaSegment, nextMediaSequence, /* partIndex= */
C.INDEX_UNSET);
}
if (nextPartIndex < mediaSegment.parts.size()) {
// The requested part is available in the requested segment.
return new SegmentBaseHolder(mediaSegment.parts.get(nextPartIndex), nextMediaSequence, nextPartIndex);
} else if (segmentIndexInPlaylist + 1 < mediaPlaylist.segments.size()) {
// The first part of the next segment is requested, but we can use the next full segment.
return new SegmentBaseHolder(mediaPlaylist.segments.get(segmentIndexInPlaylist + 1), nextMediaSequence + 1, /* partIndex= */
C.INDEX_UNSET);
} else if (!mediaPlaylist.trailingParts.isEmpty()) {
// The part index is rolling over to the first trailing part.
return new SegmentBaseHolder(mediaPlaylist.trailingParts.get(0), nextMediaSequence + 1, /* partIndex= */
0);
}
// End of stream.
return null;
}
use of com.google.android.exoplayer2.source.hls.playlist.HlsMediaPlaylist.Part in project ExoPlayer by google.
the class HlsChunkSource method getChunkPublicationState.
/**
* Returns the publication state of the given chunk.
*
* @param mediaChunk The media chunk for which to evaluate the publication state.
* @return Whether the media chunk is {@link #CHUNK_PUBLICATION_STATE_PRELOAD a preload chunk},
* has been {@link #CHUNK_PUBLICATION_STATE_REMOVED removed} or is definitely {@link
* #CHUNK_PUBLICATION_STATE_PUBLISHED published}.
*/
@ChunkPublicationState
public int getChunkPublicationState(HlsMediaChunk mediaChunk) {
if (mediaChunk.partIndex == C.INDEX_UNSET) {
// Chunks based on full segments can't be removed and are always published.
return CHUNK_PUBLICATION_STATE_PUBLISHED;
}
Uri playlistUrl = playlistUrls[trackGroup.indexOf(mediaChunk.trackFormat)];
HlsMediaPlaylist mediaPlaylist = checkNotNull(playlistTracker.getPlaylistSnapshot(playlistUrl, /* isForPlayback= */
false));
int segmentIndexInPlaylist = (int) (mediaChunk.chunkIndex - mediaPlaylist.mediaSequence);
if (segmentIndexInPlaylist < 0) {
// The parent segment of the previous chunk is not in the current playlist anymore.
return CHUNK_PUBLICATION_STATE_PUBLISHED;
}
List<HlsMediaPlaylist.Part> partsInCurrentPlaylist = segmentIndexInPlaylist < mediaPlaylist.segments.size() ? mediaPlaylist.segments.get(segmentIndexInPlaylist).parts : mediaPlaylist.trailingParts;
if (mediaChunk.partIndex >= partsInCurrentPlaylist.size()) {
// sequence in the new playlist.
return CHUNK_PUBLICATION_STATE_REMOVED;
}
HlsMediaPlaylist.Part newPart = partsInCurrentPlaylist.get(mediaChunk.partIndex);
if (newPart.isPreload) {
// The playlist did not change and the part in the new playlist is still a preload hint.
return CHUNK_PUBLICATION_STATE_PRELOAD;
}
Uri newUri = Uri.parse(UriUtil.resolve(mediaPlaylist.baseUri, newPart.url));
return Util.areEqual(newUri, mediaChunk.dataSpec.uri) ? CHUNK_PUBLICATION_STATE_PUBLISHED : CHUNK_PUBLICATION_STATE_REMOVED;
}
use of com.google.android.exoplayer2.source.hls.playlist.HlsMediaPlaylist.Part in project ExoPlayer by google.
the class HlsChunkSource method getNextChunk.
/**
* Returns the next chunk to load.
*
* <p>If a chunk is available then {@link HlsChunkHolder#chunk} is set. If the end of the stream
* has been reached then {@link HlsChunkHolder#endOfStream} is set. If a chunk is not available
* but the end of the stream has not been reached, {@link HlsChunkHolder#playlistUrl} is set to
* contain the {@link Uri} that refers to the playlist that needs refreshing.
*
* @param playbackPositionUs The current playback position relative to the period start in
* microseconds. If playback of the period to which this chunk source belongs has not yet
* started, the value will be the starting position in the period minus the duration of any
* media in previous periods still to be played.
* @param loadPositionUs The current load position relative to the period start in microseconds.
* @param queue The queue of buffered {@link HlsMediaChunk}s.
* @param allowEndOfStream Whether {@link HlsChunkHolder#endOfStream} is allowed to be set for
* non-empty media playlists. If {@code false}, the last available chunk is returned instead.
* If the media playlist is empty, {@link HlsChunkHolder#endOfStream} is always set.
* @param out A holder to populate.
*/
public void getNextChunk(long playbackPositionUs, long loadPositionUs, List<HlsMediaChunk> queue, boolean allowEndOfStream, HlsChunkHolder out) {
@Nullable HlsMediaChunk previous = queue.isEmpty() ? null : Iterables.getLast(queue);
int oldTrackIndex = previous == null ? C.INDEX_UNSET : trackGroup.indexOf(previous.trackFormat);
long bufferedDurationUs = loadPositionUs - playbackPositionUs;
long timeToLiveEdgeUs = resolveTimeToLiveEdgeUs(playbackPositionUs);
if (previous != null && !independentSegments) {
// Unless segments are known to be independent, switching tracks requires downloading
// overlapping segments. Hence we subtract the previous segment's duration from the buffered
// duration.
// This may affect the live-streaming adaptive track selection logic, when we compare the
// buffered duration to time-to-live-edge to decide whether to switch. Therefore, we subtract
// the duration of the last loaded segment from timeToLiveEdgeUs as well.
long subtractedDurationUs = previous.getDurationUs();
bufferedDurationUs = max(0, bufferedDurationUs - subtractedDurationUs);
if (timeToLiveEdgeUs != C.TIME_UNSET) {
timeToLiveEdgeUs = max(0, timeToLiveEdgeUs - subtractedDurationUs);
}
}
// Select the track.
MediaChunkIterator[] mediaChunkIterators = createMediaChunkIterators(previous, loadPositionUs);
trackSelection.updateSelectedTrack(playbackPositionUs, bufferedDurationUs, timeToLiveEdgeUs, queue, mediaChunkIterators);
int selectedTrackIndex = trackSelection.getSelectedIndexInTrackGroup();
boolean switchingTrack = oldTrackIndex != selectedTrackIndex;
Uri selectedPlaylistUrl = playlistUrls[selectedTrackIndex];
if (!playlistTracker.isSnapshotValid(selectedPlaylistUrl)) {
out.playlistUrl = selectedPlaylistUrl;
seenExpectedPlaylistError &= selectedPlaylistUrl.equals(expectedPlaylistUrl);
expectedPlaylistUrl = selectedPlaylistUrl;
// Retry when playlist is refreshed.
return;
}
@Nullable HlsMediaPlaylist playlist = playlistTracker.getPlaylistSnapshot(selectedPlaylistUrl, /* isForPlayback= */
true);
// playlistTracker snapshot is valid (checked by if() above), so playlist must be non-null.
checkNotNull(playlist);
independentSegments = playlist.hasIndependentSegments;
updateLiveEdgeTimeUs(playlist);
// Select the chunk.
long startOfPlaylistInPeriodUs = playlist.startTimeUs - playlistTracker.getInitialStartTimeUs();
Pair<Long, Integer> nextMediaSequenceAndPartIndex = getNextMediaSequenceAndPartIndex(previous, switchingTrack, playlist, startOfPlaylistInPeriodUs, loadPositionUs);
long chunkMediaSequence = nextMediaSequenceAndPartIndex.first;
int partIndex = nextMediaSequenceAndPartIndex.second;
if (chunkMediaSequence < playlist.mediaSequence && previous != null && switchingTrack) {
// We try getting the next chunk without adapting in case that's the reason for falling
// behind the live window.
selectedTrackIndex = oldTrackIndex;
selectedPlaylistUrl = playlistUrls[selectedTrackIndex];
playlist = playlistTracker.getPlaylistSnapshot(selectedPlaylistUrl, /* isForPlayback= */
true);
// playlistTracker snapshot is valid (checked by if() above), so playlist must be non-null.
checkNotNull(playlist);
startOfPlaylistInPeriodUs = playlist.startTimeUs - playlistTracker.getInitialStartTimeUs();
// Get the next segment/part without switching tracks.
Pair<Long, Integer> nextMediaSequenceAndPartIndexWithoutAdapting = getNextMediaSequenceAndPartIndex(previous, /* switchingTrack= */
false, playlist, startOfPlaylistInPeriodUs, loadPositionUs);
chunkMediaSequence = nextMediaSequenceAndPartIndexWithoutAdapting.first;
partIndex = nextMediaSequenceAndPartIndexWithoutAdapting.second;
}
if (chunkMediaSequence < playlist.mediaSequence) {
fatalError = new BehindLiveWindowException();
return;
}
@Nullable SegmentBaseHolder segmentBaseHolder = getNextSegmentHolder(playlist, chunkMediaSequence, partIndex);
if (segmentBaseHolder == null) {
if (!playlist.hasEndTag) {
// Reload the playlist in case of a live stream.
out.playlistUrl = selectedPlaylistUrl;
seenExpectedPlaylistError &= selectedPlaylistUrl.equals(expectedPlaylistUrl);
expectedPlaylistUrl = selectedPlaylistUrl;
return;
} else if (allowEndOfStream || playlist.segments.isEmpty()) {
out.endOfStream = true;
return;
}
// Use the last segment available in case of a VOD stream.
segmentBaseHolder = new SegmentBaseHolder(Iterables.getLast(playlist.segments), playlist.mediaSequence + playlist.segments.size() - 1, /* partIndex= */
C.INDEX_UNSET);
}
// We have a valid media segment, we can discard any playlist errors at this point.
seenExpectedPlaylistError = false;
expectedPlaylistUrl = null;
// Check if the media segment or its initialization segment are fully encrypted.
@Nullable Uri initSegmentKeyUri = getFullEncryptionKeyUri(playlist, segmentBaseHolder.segmentBase.initializationSegment);
out.chunk = maybeCreateEncryptionChunkFor(initSegmentKeyUri, selectedTrackIndex);
if (out.chunk != null) {
return;
}
@Nullable Uri mediaSegmentKeyUri = getFullEncryptionKeyUri(playlist, segmentBaseHolder.segmentBase);
out.chunk = maybeCreateEncryptionChunkFor(mediaSegmentKeyUri, selectedTrackIndex);
if (out.chunk != null) {
return;
}
boolean shouldSpliceIn = HlsMediaChunk.shouldSpliceIn(previous, selectedPlaylistUrl, playlist, segmentBaseHolder, startOfPlaylistInPeriodUs);
if (shouldSpliceIn && segmentBaseHolder.isPreload) {
// becomes fully available (or the track selection selects another track).
return;
}
out.chunk = HlsMediaChunk.createInstance(extractorFactory, mediaDataSource, playlistFormats[selectedTrackIndex], startOfPlaylistInPeriodUs, playlist, segmentBaseHolder, selectedPlaylistUrl, muxedCaptionFormats, trackSelection.getSelectionReason(), trackSelection.getSelectionData(), isTimestampMaster, timestampAdjusterProvider, previous, /* mediaSegmentKey= */
keyCache.get(mediaSegmentKeyUri), /* initSegmentKey= */
keyCache.get(initSegmentKeyUri), shouldSpliceIn, playerId);
}
Aggregations