Merge pull request #9536 from TiVo:p-fix-issue-2882

PiperOrigin-RevId: 411056555
This commit is contained in:
Ian Baker 2021-11-22 17:00:01 +00:00
commit 30caac6fc3
8 changed files with 298 additions and 4 deletions

View File

@ -33,6 +33,7 @@ import androidx.media3.common.util.Util;
import androidx.media3.datasource.DataSource; import androidx.media3.datasource.DataSource;
import androidx.media3.datasource.DataSpec; import androidx.media3.datasource.DataSpec;
import androidx.media3.datasource.TransferListener; import androidx.media3.datasource.TransferListener;
import androidx.media3.exoplayer.SeekParameters;
import androidx.media3.exoplayer.analytics.PlayerId; import androidx.media3.exoplayer.analytics.PlayerId;
import androidx.media3.exoplayer.hls.playlist.HlsMediaPlaylist; import androidx.media3.exoplayer.hls.playlist.HlsMediaPlaylist;
import androidx.media3.exoplayer.hls.playlist.HlsMediaPlaylist.Segment; import androidx.media3.exoplayer.hls.playlist.HlsMediaPlaylist.Segment;
@ -241,6 +242,53 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
this.isTimestampMaster = isTimestampMaster; this.isTimestampMaster = isTimestampMaster;
} }
/**
* 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[selectedIndex], /* 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;
}
/** /**
* Returns the publication state of the given chunk. * Returns the publication state of the given chunk.
* *

View File

@ -423,7 +423,14 @@ public final class HlsMediaPeriod
@Override @Override
public long getAdjustedSeekPositionUs(long positionUs, SeekParameters seekParameters) { public long getAdjustedSeekPositionUs(long positionUs, SeekParameters seekParameters) {
return positionUs; long seekTargetUs = positionUs;
for (HlsSampleStreamWrapper sampleStreamWrapper : enabledSampleStreamWrappers) {
if (sampleStreamWrapper.isVideoSampleStream()) {
seekTargetUs = sampleStreamWrapper.getAdjustedSeekPositionUs(positionUs, seekParameters);
break;
}
}
return seekTargetUs;
} }
// HlsSampleStreamWrapper.Callback implementation. // HlsSampleStreamWrapper.Callback implementation.

View File

@ -41,6 +41,7 @@ import androidx.media3.common.util.Util;
import androidx.media3.datasource.HttpDataSource; import androidx.media3.datasource.HttpDataSource;
import androidx.media3.decoder.DecoderInputBuffer; import androidx.media3.decoder.DecoderInputBuffer;
import androidx.media3.exoplayer.FormatHolder; import androidx.media3.exoplayer.FormatHolder;
import androidx.media3.exoplayer.SeekParameters;
import androidx.media3.exoplayer.drm.DrmSession; import androidx.media3.exoplayer.drm.DrmSession;
import androidx.media3.exoplayer.drm.DrmSessionEventListener; import androidx.media3.exoplayer.drm.DrmSessionEventListener;
import androidx.media3.exoplayer.drm.DrmSessionManager; import androidx.media3.exoplayer.drm.DrmSessionManager;
@ -584,6 +585,22 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
&& exclusionDurationMs != C.TIME_UNSET; && exclusionDurationMs != C.TIME_UNSET;
} }
/** Returns whether the primary sample stream is {@link C#TRACK_TYPE_VIDEO}. */
public boolean isVideoSampleStream() {
return primarySampleQueueType == C.TRACK_TYPE_VIDEO;
}
/**
* 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) {
return chunkSource.getAdjustedSeekPositionUs(positionUs, seekParameters);
}
// SampleStream implementation. // SampleStream implementation.
public boolean isReady(int sampleQueueIndex) { public boolean isReady(int sampleQueueIndex) {

View File

@ -0,0 +1,183 @@
/*
* Copyright 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.media3.exoplayer.hls;
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.when;
import android.net.Uri;
import androidx.media3.common.Format;
import androidx.media3.exoplayer.SeekParameters;
import androidx.media3.exoplayer.analytics.PlayerId;
import androidx.media3.exoplayer.hls.playlist.HlsMediaPlaylist;
import androidx.media3.exoplayer.hls.playlist.HlsPlaylistParser;
import androidx.media3.exoplayer.hls.playlist.HlsPlaylistTracker;
import androidx.media3.test.utils.ExoPlayerTestRunner;
import androidx.media3.test.utils.FakeDataSource;
import androidx.media3.test.utils.TestUtil;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import java.io.IOException;
import java.io.InputStream;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.Mockito;
/** Unit tests for {@link HlsChunkSource}. */
@RunWith(AndroidJUnit4.class)
public class HlsChunkSourceTest {
private static final String PLAYLIST = "media/m3u8/media_playlist";
private static final String PLAYLIST_INDEPENDENT_SEGMENTS =
"media/m3u8/media_playlist_independent_segments";
private static final String PLAYLIST_EMPTY = "media/m3u8/media_playlist_empty";
private static final Uri PLAYLIST_URI = Uri.parse("http://example.com/");
private static final long PLAYLIST_START_PERIOD_OFFSET_US = 8_000_000L;
private final HlsExtractorFactory mockExtractorFactory = HlsExtractorFactory.DEFAULT;
@Mock private HlsPlaylistTracker mockPlaylistTracker;
private HlsChunkSource testChunkSource;
@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(
mockExtractorFactory,
mockPlaylistTracker,
new Uri[] {PLAYLIST_URI},
new 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);
}
@Test
public void getAdjustedSeekPositionUs_previousSync() {
long adjustedPositionUs =
testChunkSource.getAdjustedSeekPositionUs(
playlistTimeToPeriodTimeUs(17_000_000), SeekParameters.PREVIOUS_SYNC);
assertThat(periodTimeToPlaylistTimeUs(adjustedPositionUs)).isEqualTo(16_000_000);
}
@Test
public void getAdjustedSeekPositionUs_nextSync() {
long adjustedPositionUs =
testChunkSource.getAdjustedSeekPositionUs(
playlistTimeToPeriodTimeUs(17_000_000), SeekParameters.NEXT_SYNC);
assertThat(periodTimeToPlaylistTimeUs(adjustedPositionUs)).isEqualTo(20_000_000);
}
@Test
public void getAdjustedSeekPositionUs_nextSyncAtEnd() {
long adjustedPositionUs =
testChunkSource.getAdjustedSeekPositionUs(
playlistTimeToPeriodTimeUs(24_000_000), SeekParameters.NEXT_SYNC);
assertThat(periodTimeToPlaylistTimeUs(adjustedPositionUs)).isEqualTo(24_000_000);
}
@Test
public void getAdjustedSeekPositionUs_closestSyncBefore() {
long adjustedPositionUs =
testChunkSource.getAdjustedSeekPositionUs(
playlistTimeToPeriodTimeUs(17_000_000), SeekParameters.CLOSEST_SYNC);
assertThat(periodTimeToPlaylistTimeUs(adjustedPositionUs)).isEqualTo(16_000_000);
}
@Test
public void getAdjustedSeekPositionUs_closestSyncAfter() {
long adjustedPositionUs =
testChunkSource.getAdjustedSeekPositionUs(
playlistTimeToPeriodTimeUs(19_000_000), SeekParameters.CLOSEST_SYNC);
assertThat(periodTimeToPlaylistTimeUs(adjustedPositionUs)).isEqualTo(20_000_000);
}
@Test
public void getAdjustedSeekPositionUs_exact() {
long adjustedPositionUs =
testChunkSource.getAdjustedSeekPositionUs(
playlistTimeToPeriodTimeUs(17_000_000), SeekParameters.EXACT);
assertThat(periodTimeToPlaylistTimeUs(adjustedPositionUs)).isEqualTo(17_000_000);
}
@Test
public void getAdjustedSeekPositionUs_noIndependentSegments() throws IOException {
InputStream inputStream =
TestUtil.getInputStream(ApplicationProvider.getApplicationContext(), PLAYLIST);
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);
}
@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);
}
private static long playlistTimeToPeriodTimeUs(long playlistTimeUs) {
return playlistTimeUs + PLAYLIST_START_PERIOD_OFFSET_US;
}
private static long periodTimeToPlaylistTimeUs(long periodTimeUs) {
return periodTimeUs - PLAYLIST_START_PERIOD_OFFSET_US;
}
}

View File

@ -0,0 +1,16 @@
#EXTM3U
#EXT-X-MEDIA-SEQUENCE:2
#EXT-X-MAP:URI="init.mp4"
#EXTINF:4,
2.mp4
#EXTINF:4,
3.mp4
#EXTINF:4,
4.mp4
#EXTINF:4,
5.mp4
#EXTINF:4,
6.mp4
#EXTINF:4,
7.mp4
#EXT-X-ENDLIST

View File

@ -0,0 +1,3 @@
#EXTM3U
#EXT-X-MEDIA-SEQUENCE:2
#EXT-X-MAP:URI="init.mp4"

View File

@ -0,0 +1,17 @@
#EXTM3U
#EXT-X-MEDIA-SEQUENCE:2
#EXT-X-INDEPENDENT-SEGMENTS
#EXT-X-MAP:URI="init.mp4"
#EXTINF:4,
2.mp4
#EXTINF:4,
3.mp4
#EXTINF:4,
4.mp4
#EXTINF:4,
5.mp4
#EXTINF:4,
6.mp4
#EXTINF:4,
7.mp4
#EXT-X-ENDLIST

View File

@ -33,7 +33,6 @@ import androidx.media3.test.utils.FakeDataSet.FakeData;
import androidx.media3.test.utils.FakeDataSet.FakeData.Segment; import androidx.media3.test.utils.FakeDataSet.FakeData.Segment;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
/** /**
* A fake {@link DataSource} capable of simulating various scenarios. It uses a {@link FakeDataSet} * A fake {@link DataSource} capable of simulating various scenarios. It uses a {@link FakeDataSet}
@ -45,9 +44,13 @@ public class FakeDataSource extends BaseDataSource {
/** Factory to create a {@link FakeDataSource}. */ /** Factory to create a {@link FakeDataSource}. */
public static class Factory implements DataSource.Factory { public static class Factory implements DataSource.Factory {
protected @MonotonicNonNull FakeDataSet fakeDataSet; protected FakeDataSet fakeDataSet;
protected boolean isNetwork; protected boolean isNetwork;
public Factory() {
fakeDataSet = new FakeDataSet();
}
public final Factory setFakeDataSet(FakeDataSet fakeDataSet) { public final Factory setFakeDataSet(FakeDataSet fakeDataSet) {
this.fakeDataSet = fakeDataSet; this.fakeDataSet = fakeDataSet;
return this; return this;
@ -60,7 +63,7 @@ public class FakeDataSource extends BaseDataSource {
@Override @Override
public FakeDataSource createDataSource() { public FakeDataSource createDataSource() {
return new FakeDataSource(Assertions.checkStateNotNull(fakeDataSet), isNetwork); return new FakeDataSource(fakeDataSet, isNetwork);
} }
} }