Merge pull request #9536 from TiVo:p-fix-issue-2882
PiperOrigin-RevId: 411056555
This commit is contained in:
commit
4a69e1660f
@ -11,6 +11,9 @@
|
||||
* Add a method to `AdPlaybackState` to allow resetting an ad group so that
|
||||
it can be played again
|
||||
([#9615](https://github.com/google/ExoPlayer/issues/9615)).
|
||||
* HLS:
|
||||
* Support key-frame accurate seeking in HLS
|
||||
([#2882](https://github.com/google/ExoPlayer/issues/2882)).
|
||||
|
||||
### 2.16.1 (2021-11-18)
|
||||
|
||||
|
@ -26,6 +26,7 @@ import androidx.annotation.Nullable;
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.Format;
|
||||
import com.google.android.exoplayer2.SeekParameters;
|
||||
import com.google.android.exoplayer2.analytics.PlayerId;
|
||||
import com.google.android.exoplayer2.source.BehindLiveWindowException;
|
||||
import com.google.android.exoplayer2.source.TrackGroup;
|
||||
@ -241,6 +242,53 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
|
||||
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.
|
||||
*
|
||||
|
@ -421,7 +421,14 @@ public final class HlsMediaPeriod
|
||||
|
||||
@Override
|
||||
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.
|
||||
|
@ -29,6 +29,7 @@ import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.Format;
|
||||
import com.google.android.exoplayer2.FormatHolder;
|
||||
import com.google.android.exoplayer2.ParserException;
|
||||
import com.google.android.exoplayer2.SeekParameters;
|
||||
import com.google.android.exoplayer2.decoder.DecoderInputBuffer;
|
||||
import com.google.android.exoplayer2.drm.DrmInitData;
|
||||
import com.google.android.exoplayer2.drm.DrmSession;
|
||||
@ -584,6 +585,22 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
|
||||
&& 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.
|
||||
|
||||
public boolean isReady(int sampleQueueIndex) {
|
||||
|
@ -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 com.google.android.exoplayer2.source.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.test.core.app.ApplicationProvider;
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
import com.google.android.exoplayer2.Format;
|
||||
import com.google.android.exoplayer2.SeekParameters;
|
||||
import com.google.android.exoplayer2.analytics.PlayerId;
|
||||
import com.google.android.exoplayer2.source.hls.playlist.HlsMediaPlaylist;
|
||||
import com.google.android.exoplayer2.source.hls.playlist.HlsPlaylistParser;
|
||||
import com.google.android.exoplayer2.source.hls.playlist.HlsPlaylistTracker;
|
||||
import com.google.android.exoplayer2.testutil.ExoPlayerTestRunner;
|
||||
import com.google.android.exoplayer2.testutil.FakeDataSource;
|
||||
import com.google.android.exoplayer2.testutil.TestUtil;
|
||||
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;
|
||||
}
|
||||
}
|
16
testdata/src/test/assets/media/m3u8/media_playlist
vendored
Normal file
16
testdata/src/test/assets/media/m3u8/media_playlist
vendored
Normal 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
|
3
testdata/src/test/assets/media/m3u8/media_playlist_empty
vendored
Normal file
3
testdata/src/test/assets/media/m3u8/media_playlist_empty
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
#EXTM3U
|
||||
#EXT-X-MEDIA-SEQUENCE:2
|
||||
#EXT-X-MAP:URI="init.mp4"
|
17
testdata/src/test/assets/media/m3u8/media_playlist_independent_segments
vendored
Normal file
17
testdata/src/test/assets/media/m3u8/media_playlist_independent_segments
vendored
Normal 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
|
@ -32,7 +32,6 @@ import com.google.android.exoplayer2.util.Assertions;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
import java.io.IOException;
|
||||
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}
|
||||
@ -43,9 +42,13 @@ public class FakeDataSource extends BaseDataSource {
|
||||
/** Factory to create a {@link FakeDataSource}. */
|
||||
public static class Factory implements DataSource.Factory {
|
||||
|
||||
protected @MonotonicNonNull FakeDataSet fakeDataSet;
|
||||
protected FakeDataSet fakeDataSet;
|
||||
protected boolean isNetwork;
|
||||
|
||||
public Factory() {
|
||||
fakeDataSet = new FakeDataSet();
|
||||
}
|
||||
|
||||
public final Factory setFakeDataSet(FakeDataSet fakeDataSet) {
|
||||
this.fakeDataSet = fakeDataSet;
|
||||
return this;
|
||||
@ -58,7 +61,7 @@ public class FakeDataSource extends BaseDataSource {
|
||||
|
||||
@Override
|
||||
public FakeDataSource createDataSource() {
|
||||
return new FakeDataSource(Assertions.checkStateNotNull(fakeDataSet), isNetwork);
|
||||
return new FakeDataSource(fakeDataSet, isNetwork);
|
||||
}
|
||||
}
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user