mirror of
https://github.com/androidx/media.git
synced 2025-04-30 06:46:50 +08:00
Merge pull request #9536 from TiVo:p-fix-issue-2882
PiperOrigin-RevId: 411056555
This commit is contained in:
commit
30caac6fc3
@ -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.
|
||||||
*
|
*
|
||||||
|
@ -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.
|
||||||
|
@ -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) {
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
@ -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
|
@ -0,0 +1,3 @@
|
|||||||
|
#EXTM3U
|
||||||
|
#EXT-X-MEDIA-SEQUENCE:2
|
||||||
|
#EXT-X-MAP:URI="init.mp4"
|
@ -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
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user