use of androidx.media3.exoplayer.hls.playlist.HlsMediaPlaylist in project media by androidx.
the class HlsChunkSource method getAdjustedSeekPositionUs.
/**
* Adjusts a seek position given the specified {@link SeekParameters}.
*
* @param positionUs The seek position in microseconds.
* @param seekParameters Parameters that control how the seek is performed.
* @return The adjusted seek position, in microseconds.
*/
public long getAdjustedSeekPositionUs(long positionUs, SeekParameters seekParameters) {
int selectedIndex = trackSelection.getSelectedIndex();
@Nullable HlsMediaPlaylist mediaPlaylist = selectedIndex < playlistUrls.length && selectedIndex != C.INDEX_UNSET ? playlistTracker.getPlaylistSnapshot(playlistUrls[trackSelection.getSelectedIndexInTrackGroup()], /* isForPlayback= */
true) : null;
if (mediaPlaylist == null || mediaPlaylist.segments.isEmpty() || !mediaPlaylist.hasIndependentSegments) {
return positionUs;
}
// Segments start with sync samples (i.e., EXT-X-INDEPENDENT-SEGMENTS is set) and the playlist
// is non-empty, so we can use segment start times as sync points. Note that in the rare case
// that (a) an adaptive quality switch occurs between the adjustment and the seek being
// performed, and (b) segment start times are not aligned across variants, it's possible that
// the adjusted position may not be at a sync point when it was intended to be. However, this is
// very much an edge case, and getting it wrong is worth it for getting the vast majority of
// cases right whilst keeping the implementation relatively simple.
long startOfPlaylistInPeriodUs = mediaPlaylist.startTimeUs - playlistTracker.getInitialStartTimeUs();
long relativePositionUs = positionUs - startOfPlaylistInPeriodUs;
int segmentIndex = Util.binarySearchFloor(mediaPlaylist.segments, relativePositionUs, /* inclusive= */
true, /* stayInBounds= */
true);
long firstSyncUs = mediaPlaylist.segments.get(segmentIndex).relativeStartTimeUs;
long secondSyncUs = firstSyncUs;
if (segmentIndex != mediaPlaylist.segments.size() - 1) {
secondSyncUs = mediaPlaylist.segments.get(segmentIndex + 1).relativeStartTimeUs;
}
return seekParameters.resolveSeekPositionUs(relativePositionUs, firstSyncUs, secondSyncUs) + startOfPlaylistInPeriodUs;
}
use of androidx.media3.exoplayer.hls.playlist.HlsMediaPlaylist in project media by androidx.
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 androidx.media3.exoplayer.hls.playlist.HlsMediaPlaylist in project media by androidx.
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);
}
use of androidx.media3.exoplayer.hls.playlist.HlsMediaPlaylist in project media by androidx.
the class HlsChunkSourceTest method setup.
@Before
public void setup() throws IOException {
mockPlaylistTracker = Mockito.mock(HlsPlaylistTracker.class);
InputStream inputStream = TestUtil.getInputStream(ApplicationProvider.getApplicationContext(), PLAYLIST_INDEPENDENT_SEGMENTS);
HlsMediaPlaylist playlist = (HlsMediaPlaylist) new HlsPlaylistParser().parse(PLAYLIST_URI, inputStream);
when(mockPlaylistTracker.getPlaylistSnapshot(eq(PLAYLIST_URI), anyBoolean())).thenReturn(playlist);
testChunkSource = new HlsChunkSource(HlsExtractorFactory.DEFAULT, mockPlaylistTracker, new Uri[] { IFRAME_URI, PLAYLIST_URI }, new Format[] { IFRAME_FORMAT, ExoPlayerTestRunner.VIDEO_FORMAT }, new DefaultHlsDataSourceFactory(new FakeDataSource.Factory()), /* mediaTransferListener= */
null, new TimestampAdjusterProvider(), /* muxedCaptionFormats= */
null, PlayerId.UNSET);
when(mockPlaylistTracker.isSnapshotValid(eq(PLAYLIST_URI))).thenReturn(true);
// Mock that segments totalling PLAYLIST_START_PERIOD_OFFSET_US in duration have been removed
// from the start of the playlist.
when(mockPlaylistTracker.getInitialStartTimeUs()).thenReturn(playlist.startTimeUs - PLAYLIST_START_PERIOD_OFFSET_US);
}
use of androidx.media3.exoplayer.hls.playlist.HlsMediaPlaylist in project media by androidx.
the class HlsChunkSourceTest method getAdjustedSeekPositionUs_emptyPlaylist.
@Test
public void getAdjustedSeekPositionUs_emptyPlaylist() throws IOException {
InputStream inputStream = TestUtil.getInputStream(ApplicationProvider.getApplicationContext(), PLAYLIST_EMPTY);
HlsMediaPlaylist playlist = (HlsMediaPlaylist) new HlsPlaylistParser().parse(PLAYLIST_URI, inputStream);
when(mockPlaylistTracker.getPlaylistSnapshot(eq(PLAYLIST_URI), anyBoolean())).thenReturn(playlist);
long adjustedPositionUs = testChunkSource.getAdjustedSeekPositionUs(playlistTimeToPeriodTimeUs(100_000_000), SeekParameters.EXACT);
assertThat(periodTimeToPlaylistTimeUs(adjustedPositionUs)).isEqualTo(100_000_000);
}
Aggregations