Skip to content

Commit

Permalink
Implements SeekParameters.*_SYNC variants for HLS
Browse files Browse the repository at this point in the history
The HLS implementation of `getAdjustedSeekPositionUs()` now completely supports `SeekParameters.CLOSEST_SYNC` and it's brotheran, assuming the HLS stream indicates segments all start with an IDR (that is EXT-X-INDEPENDENT-SEGMENTS  is specified).

This fixes issue google#2882 and improves (but does not completely solve google#8592
  • Loading branch information
stevemayhew committed Oct 6, 2021
1 parent 03ff5b6 commit 06321b4
Show file tree
Hide file tree
Showing 4 changed files with 216 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
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.source.BehindLiveWindowException;
import com.google.android.exoplayer2.source.TrackGroup;
import com.google.android.exoplayer2.source.chunk.BaseMediaChunkIterator;
Expand Down Expand Up @@ -237,6 +238,31 @@ public void setIsTimestampMaster(boolean isTimestampMaster) {
this.isTimestampMaster = isTimestampMaster;
}

long getAdjustedSeekPositionUs(long positionUs, SeekParameters seekParameters) {
long adjustedPositionUs = positionUs;

int selectedIndex = trackSelection.getSelectedIndex();
boolean haveTrackSelection = selectedIndex < playlistUrls.length && selectedIndex != C.INDEX_UNSET;
@Nullable HlsMediaPlaylist mediaPlaylist = null;
if (haveTrackSelection) {
mediaPlaylist = playlistTracker.getPlaylistSnapshot(playlistUrls[selectedIndex], /* isForPlayback= */ true);
}

// Resolve to a segment boundary, current track is fine (all should be same).
// and, segments must start with sync (EXT-X-INDEPENDENT-SEGMENTS must be present)
if (mediaPlaylist != null && mediaPlaylist.hasIndependentSegments) {
int segIndex = Util.binarySearchFloor(mediaPlaylist.segments, positionUs, true, true);
long firstSyncUs = mediaPlaylist.segments.get(segIndex).relativeStartTimeUs;
long secondSyncUs = firstSyncUs;
if (segIndex != mediaPlaylist.segments.size() - 1) {
secondSyncUs = mediaPlaylist.segments.get(segIndex + 1).relativeStartTimeUs;
}
adjustedPositionUs = seekParameters.resolveSeekPositionUs(positionUs, firstSyncUs, secondSyncUs);
}

return adjustedPositionUs;
}

/**
* Returns the publication state of the given chunk.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -417,7 +417,14 @@ public long seekToUs(long positionUs) {

@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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
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;
Expand Down Expand Up @@ -585,6 +586,16 @@ public boolean onPlaylistError(Uri playlistUrl, LoadErrorInfo loadErrorInfo, boo
&& exclusionDurationMs != C.TIME_UNSET;
}

public boolean isVideoSampleStream() {
return primarySampleQueueType == C.TRACK_TYPE_VIDEO;
}


public long getAdjustedSeekPositionUs(long positionUs, SeekParameters seekParameters) {
return chunkSource.getAdjustedSeekPositionUs(positionUs, seekParameters);
}


// SampleStream implementation.

public boolean isReady(int sampleQueueIndex) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
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.ext.junit.runners.AndroidJUnit4;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collections;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;

import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.SeekParameters;
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.util.Util;


@RunWith(AndroidJUnit4.class)
public class HlsChunkSourceTest {

public static final String TEST_PLAYLIST = "#EXTM3U\n" +
"#EXT-X-MEDIA-SEQUENCE:1606273114\n" +
"#EXT-X-VERSION:6\n" +
"#EXT-X-PLAYLIST-TYPE:VOD\n" +
"#EXT-X-I-FRAMES-ONLY\n" +
"#EXT-X-INDEPENDENT-SEGMENTS\n" +
"#EXT-X-MAP:URI=\"init-CCUR_iframe.tsv\"\n" +
"#EXT-X-PROGRAM-DATE-TIME:2020-11-25T02:58:34+00:00\n" +
"#EXTINF:4,\n" +
"#EXT-X-BYTERANGE:52640@19965036\n" +
"1606272900-CCUR_iframe.tsv\n" +
"#EXTINF:4,\n" +
"#EXT-X-BYTERANGE:77832@20253992\n" +
"1606272900-CCUR_iframe.tsv\n" +
"#EXTINF:4,\n" +
"#EXT-X-BYTERANGE:168824@21007496\n" +
"1606272900-CCUR_iframe.tsv\n" +
"#EXTINF:4,\n" +
"#EXT-X-BYTERANGE:177848@21888840\n" +
"1606272900-CCUR_iframe.tsv\n" +
"#EXTINF:4,\n" +
"#EXT-X-BYTERANGE:69560@22496456\n" +
"1606272900-CCUR_iframe.tsv\n" +
"#EXTINF:4,\n" +
"#EXT-X-BYTERANGE:41360@22830156\n" +
"1606272900-CCUR_iframe.tsv\n" +
"#EXT-X-ENDLIST\n" +
"\n";
public static final Uri PLAYLIST_URI = Uri.parse("http://example.com/");

@Mock
private HlsExtractorFactory mockExtractorFactory;

@Mock
private HlsPlaylistTracker mockPlaylistTracker;

@Mock
private HlsDataSourceFactory mockDataSourceFactory;
private HlsChunkSource testee;

@Before
public void setup() throws IOException {
// sadly, auto mock does not work, you get NoClassDefFoundError: com/android/dx/rop/type/Type
// MockitoAnnotations.initMocks(this);
mockExtractorFactory = Mockito.mock(HlsExtractorFactory.class);
mockPlaylistTracker = Mockito.mock(HlsPlaylistTracker.class);
mockDataSourceFactory = Mockito.mock(HlsDataSourceFactory.class);
InputStream inputStream = new ByteArrayInputStream(Util.getUtf8Bytes(TEST_PLAYLIST));
HlsMediaPlaylist playlist =
(HlsMediaPlaylist) new HlsPlaylistParser().parse(PLAYLIST_URI, inputStream);

when(mockPlaylistTracker.getPlaylistSnapshot(eq(PLAYLIST_URI), anyBoolean())).thenReturn(playlist);

testee = new HlsChunkSource(
mockExtractorFactory,
mockPlaylistTracker,
new Uri[] {PLAYLIST_URI},
new Format[] { ExoPlayerTestRunner.VIDEO_FORMAT },
mockDataSourceFactory,
null,
new TimestampAdjusterProvider(),
null);

assertThat(testee).isNotNull();
}

@Test
public void getAdjustedSeekPositionUs_PreviousSync() {
when(mockPlaylistTracker.isSnapshotValid(eq(PLAYLIST_URI))).thenReturn(true);

long adjusted = testee.getAdjustedSeekPositionUs(17_000_000, SeekParameters.PREVIOUS_SYNC);
assertThat(adjusted).isEqualTo(16_000_000);
}

@Test
public void getAdjustedSeekPositionUs_NextSync() {
when(mockPlaylistTracker.isSnapshotValid(eq(PLAYLIST_URI))).thenReturn(true);

long adjusted = testee.getAdjustedSeekPositionUs(17_000_000, SeekParameters.NEXT_SYNC);
assertThat(adjusted).isEqualTo(20_000_000);
}

@Test
public void getAdjustedSeekPositionUs_NextSyncAtEnd() {
when(mockPlaylistTracker.isSnapshotValid(eq(PLAYLIST_URI))).thenReturn(true);

long adjusted = testee.getAdjustedSeekPositionUs(24_000_000, SeekParameters.NEXT_SYNC);
assertThat(adjusted).isEqualTo(24_000_000);
}

@Test
public void getAdjustedSeekPositionUs_ClosestSync() {
when(mockPlaylistTracker.isSnapshotValid(eq(PLAYLIST_URI))).thenReturn(true);

long adjusted = testee.getAdjustedSeekPositionUs(17_000_000, SeekParameters.CLOSEST_SYNC);
assertThat(adjusted).isEqualTo(16_000_000);

adjusted = testee.getAdjustedSeekPositionUs(19_000_000, SeekParameters.CLOSEST_SYNC);
assertThat(adjusted).isEqualTo(20_000_000);
}

@Test
public void getAdjustedSeekPositionUs_Exact() {
when(mockPlaylistTracker.isSnapshotValid(eq(PLAYLIST_URI))).thenReturn(true);

long adjusted = testee.getAdjustedSeekPositionUs(17_000_000, SeekParameters.EXACT);
assertThat(adjusted).isEqualTo(17_000_000);
}

@Test
public void getAdjustedSeekPositionUs_NoIndependedSegments() {
when(mockPlaylistTracker.isSnapshotValid(eq(PLAYLIST_URI))).thenReturn(true);

// difficult to mock a final class.
HlsMediaPlaylist mockPlaylist = new HlsMediaPlaylist(
HlsMediaPlaylist.PLAYLIST_TYPE_UNKNOWN,
PLAYLIST_URI.toString(),
Collections.emptyList(),
0,
0,
false,
0,
0,
8,
6,
false,
true,
true,
null,
Collections.emptyList()
);
when(mockPlaylistTracker.getPlaylistSnapshot(eq(PLAYLIST_URI), anyBoolean())).thenReturn(mockPlaylist);
long adjusted = testee.getAdjustedSeekPositionUs(100_000_000, SeekParameters.EXACT);
assertThat(adjusted).isEqualTo(100_000_000);
}

}

0 comments on commit 06321b4

Please sign in to comment.