Block HLS playlist requests at part level

Issue: #5011
PiperOrigin-RevId: 340625816
This commit is contained in:
bachinger 2020-11-04 12:47:30 +00:00 committed by Andrew Lewis
parent 4332dc2304
commit 7f49b33fea
11 changed files with 254 additions and 13 deletions

View File

@ -30,6 +30,7 @@ import com.google.android.exoplayer2.source.MediaLoadData;
import com.google.android.exoplayer2.source.MediaSourceEventListener.EventDispatcher;
import com.google.android.exoplayer2.source.hls.HlsDataSourceFactory;
import com.google.android.exoplayer2.source.hls.playlist.HlsMasterPlaylist.Variant;
import com.google.android.exoplayer2.source.hls.playlist.HlsMediaPlaylist.Part;
import com.google.android.exoplayer2.source.hls.playlist.HlsMediaPlaylist.Segment;
import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.HttpDataSource;
@ -40,6 +41,7 @@ import com.google.android.exoplayer2.upstream.Loader.LoadErrorAction;
import com.google.android.exoplayer2.upstream.ParsingLoadable;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Util;
import com.google.common.collect.Iterables;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
@ -465,6 +467,7 @@ public final class DefaultHlsPlaylistTracker
private final class MediaPlaylistBundle implements Loader.Callback<ParsingLoadable<HlsPlaylist>> {
private static final String BLOCK_MSN_PARAM = "_HLS_msn";
private static final String BLOCK_PART_PARAM = "_HLS_part";
private static final String SKIP_PARAM = "_HLS_skip";
private final Uri playlistUrl;
@ -729,16 +732,25 @@ public final class DefaultHlsPlaylistTracker
return playlistUrl;
}
Uri.Builder uriBuilder = playlistUrl.buildUpon();
if (playlistSnapshot.serverControl.skipUntilUs != C.TIME_UNSET) {
uriBuilder.appendQueryParameter(
SKIP_PARAM, playlistSnapshot.serverControl.canSkipDateRanges ? "v2" : "YES");
}
if (playlistSnapshot.serverControl.canBlockReload) {
long reloadMediaSequence =
long targetMediaSequence =
playlistSnapshot.mediaSequence
+ playlistSnapshot.segments.size()
+ playlistSnapshot.skippedSegmentCount;
uriBuilder.appendQueryParameter(BLOCK_MSN_PARAM, String.valueOf(reloadMediaSequence));
uriBuilder.appendQueryParameter(BLOCK_MSN_PARAM, String.valueOf(targetMediaSequence));
if (playlistSnapshot.partTargetDurationUs != C.TIME_UNSET) {
List<Part> trailingParts = playlistSnapshot.trailingParts;
int targetPartIndex = trailingParts.size();
if (!trailingParts.isEmpty() && Iterables.getLast(trailingParts).isPreload) {
// Ignore the preload part.
targetPartIndex--;
}
uriBuilder.appendQueryParameter(BLOCK_PART_PARAM, String.valueOf(targetPartIndex));
}
}
if (playlistSnapshot.serverControl.skipUntilUs != C.TIME_UNSET) {
uriBuilder.appendQueryParameter(
SKIP_PARAM, playlistSnapshot.serverControl.canSkipDateRanges ? "v2" : "YES");
}
return uriBuilder.build();
}

View File

@ -176,6 +176,8 @@ public final class HlsMediaPlaylist extends HlsPlaylist {
/** Whether the part is independent. */
public final boolean isIndependent;
/** Whether the part is a preloading part. */
public final boolean isPreload;
/**
* Creates an instance.
@ -192,6 +194,7 @@ public final class HlsMediaPlaylist extends HlsPlaylist {
* @param byteRangeLength See {@link #byteRangeLength}.
* @param hasGapTag See {@link #hasGapTag}.
* @param isIndependent See {@link #isIndependent}.
* @param isPreload See {@link #isPreload}.
*/
public Part(
String url,
@ -205,7 +208,8 @@ public final class HlsMediaPlaylist extends HlsPlaylist {
long byteRangeOffset,
long byteRangeLength,
boolean hasGapTag,
boolean isIndependent) {
boolean isIndependent,
boolean isPreload) {
super(
url,
initializationSegment,
@ -219,6 +223,7 @@ public final class HlsMediaPlaylist extends HlsPlaylist {
byteRangeLength,
hasGapTag);
this.isIndependent = isIndependent;
this.isPreload = isPreload;
}
}
@ -502,8 +507,13 @@ public final class HlsMediaPlaylist extends HlsPlaylist {
// The media sequences are equal.
int segmentCount = segments.size() + skippedSegmentCount;
int otherSegmentCount = other.segments.size() + other.skippedSegmentCount;
return segmentCount > otherSegmentCount
|| (segmentCount == otherSegmentCount && hasEndTag && !other.hasEndTag);
if (segmentCount != otherSegmentCount) {
return segmentCount > otherSegmentCount;
}
int partCount = trailingParts.size();
int otherPartCount = other.trailingParts.size();
return partCount > otherPartCount
|| (partCount == otherPartCount && hasEndTag && !other.hasEndTag);
}
/**

View File

@ -824,7 +824,8 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
byteRangeStart,
byteRangeLength,
/* hasGapTag= */ false,
/* isIndependent= */ false);
/* isIndependent= */ false,
/* isPreload= */ true);
} else if (line.startsWith(TAG_PART)) {
@Nullable
String segmentEncryptionIV =
@ -869,7 +870,8 @@ public final class HlsPlaylistParser implements ParsingLoadable.Parser<HlsPlayli
partByteRangeOffset,
partByteRangeLength,
isGap,
isIndependent));
isIndependent,
/* isPreload= */ false));
partStartTimeUs += partDurationUs;
if (partByteRangeLength != C.LENGTH_UNSET) {
partByteRangeOffset += partByteRangeLength;

View File

@ -64,6 +64,21 @@ public class DefaultHlsPlaylistTrackerTest {
"media/m3u8/live_low_latency_media_can_block_reload";
private static final String SAMPLE_M3U8_LIVE_MEDIA_CAN_BLOCK_RELOAD_NEXT =
"media/m3u8/live_low_latency_media_can_block_reload_next";
private static final String SAMPLE_M3U8_LIVE_MEDIA_CAN_BLOCK_RELOAD_LOW_LATENCY =
"media/m3u8/live_low_latency_media_can_block_reload_low_latency";
private static final String SAMPLE_M3U8_LIVE_MEDIA_CAN_BLOCK_RELOAD_LOW_LATENCY_NEXT =
"media/m3u8/live_low_latency_media_can_block_reload_low_latency_next";
private static final String SAMPLE_M3U8_LIVE_MEDIA_CAN_BLOCK_RELOAD_LOW_LATENCY_FULL_SEGMENT =
"media/m3u8/live_low_latency_media_can_block_reload_low_latency_full_segment";
private static final String
SAMPLE_M3U8_LIVE_MEDIA_CAN_BLOCK_RELOAD_LOW_LATENCY_FULL_SEGMENT_NEXT =
"media/m3u8/live_low_latency_media_can_block_reload_low_latency_full_segment_next";
private static final String
SAMPLE_M3U8_LIVE_MEDIA_CAN_BLOCK_RELOAD_LOW_LATENCY_FULL_SEGMENT_PRELOAD =
"media/m3u8/live_low_latency_media_can_block_reload_low_latency_full_segment_preload";
private static final String
SAMPLE_M3U8_LIVE_MEDIA_CAN_BLOCK_RELOAD_LOW_LATENCY_FULL_SEGMENT_PRELOAD_NEXT =
"media/m3u8/live_low_latency_media_can_block_reload_low_latency_full_segment_preload_next";
private static final String SAMPLE_M3U8_LIVE_MEDIA_CAN_SKIP_UNTIL_AND_BLOCK_RELOAD =
"media/m3u8/live_low_latency_media_can_skip_until_and_block_reload";
private static final String SAMPLE_M3U8_LIVE_MEDIA_CAN_SKIP_UNTIL_AND_BLOCK_RELOAD_NEXT =
@ -255,6 +270,96 @@ public class DefaultHlsPlaylistTrackerTest {
assertThat(mediaPlaylists.get(1).mediaSequence).isEqualTo(11);
}
@Test
public void
start_playlistCanBlockReloadLowLatency_requestBlockingReloadWithCorrectMediaSequenceAndPart()
throws IOException, TimeoutException, InterruptedException {
List<HttpUrl> httpUrls =
enqueueWebServerResponses(
new String[] {
"/master.m3u8",
"/media0/playlist.m3u8",
"/media0/playlist.m3u8?_HLS_msn=14&_HLS_part=1"
},
getMockResponse(SAMPLE_M3U8_LIVE_MASTER),
getMockResponse(SAMPLE_M3U8_LIVE_MEDIA_CAN_BLOCK_RELOAD_LOW_LATENCY),
getMockResponse(SAMPLE_M3U8_LIVE_MEDIA_CAN_BLOCK_RELOAD_LOW_LATENCY_NEXT));
List<HlsMediaPlaylist> mediaPlaylists =
runPlaylistTrackerAndCollectMediaPlaylists(
new DefaultHttpDataSourceFactory(),
Uri.parse(mockWebServer.url("/master.m3u8").toString()),
/* awaitedMediaPlaylistCount= */ 2);
assertRequestUrlsCalled(httpUrls);
assertThat(mediaPlaylists.get(0).mediaSequence).isEqualTo(10);
assertThat(mediaPlaylists.get(0).segments).hasSize(4);
assertThat(mediaPlaylists.get(0).trailingParts).hasSize(2);
assertThat(mediaPlaylists.get(1).mediaSequence).isEqualTo(10);
assertThat(mediaPlaylists.get(1).segments).hasSize(4);
assertThat(mediaPlaylists.get(1).trailingParts).hasSize(3);
}
@Test
public void start_playlistCanBlockReloadLowLatencyFullSegment_correctMsnAndPartParams()
throws IOException, TimeoutException, InterruptedException {
List<HttpUrl> httpUrls =
enqueueWebServerResponses(
new String[] {
"/master.m3u8",
"/media0/playlist.m3u8",
"/media0/playlist.m3u8?_HLS_msn=14&_HLS_part=0"
},
getMockResponse(SAMPLE_M3U8_LIVE_MASTER),
getMockResponse(SAMPLE_M3U8_LIVE_MEDIA_CAN_BLOCK_RELOAD_LOW_LATENCY_FULL_SEGMENT),
getMockResponse(SAMPLE_M3U8_LIVE_MEDIA_CAN_BLOCK_RELOAD_LOW_LATENCY_FULL_SEGMENT_NEXT));
List<HlsMediaPlaylist> mediaPlaylists =
runPlaylistTrackerAndCollectMediaPlaylists(
new DefaultHttpDataSourceFactory(),
Uri.parse(mockWebServer.url("/master.m3u8").toString()),
/* awaitedMediaPlaylistCount= */ 2);
assertRequestUrlsCalled(httpUrls);
assertThat(mediaPlaylists.get(0).mediaSequence).isEqualTo(10);
assertThat(mediaPlaylists.get(0).segments).hasSize(4);
assertThat(mediaPlaylists.get(0).trailingParts).isEmpty();
assertThat(mediaPlaylists.get(1).mediaSequence).isEqualTo(10);
assertThat(mediaPlaylists.get(1).segments).hasSize(4);
assertThat(mediaPlaylists.get(1).trailingParts).hasSize(1);
}
@Test
public void start_playlistCanBlockReloadLowLatencyFullSegmentWithPreloadPart_ignoresPreloadPart()
throws IOException, TimeoutException, InterruptedException {
List<HttpUrl> httpUrls =
enqueueWebServerResponses(
new String[] {
"/master.m3u8",
"/media0/playlist.m3u8",
"/media0/playlist.m3u8?_HLS_msn=14&_HLS_part=0"
},
getMockResponse(SAMPLE_M3U8_LIVE_MASTER),
getMockResponse(
SAMPLE_M3U8_LIVE_MEDIA_CAN_BLOCK_RELOAD_LOW_LATENCY_FULL_SEGMENT_PRELOAD),
getMockResponse(
SAMPLE_M3U8_LIVE_MEDIA_CAN_BLOCK_RELOAD_LOW_LATENCY_FULL_SEGMENT_PRELOAD_NEXT));
List<HlsMediaPlaylist> mediaPlaylists =
runPlaylistTrackerAndCollectMediaPlaylists(
new DefaultHttpDataSourceFactory(),
Uri.parse(mockWebServer.url("/master.m3u8").toString()),
/* awaitedMediaPlaylistCount= */ 2);
assertRequestUrlsCalled(httpUrls);
assertThat(mediaPlaylists.get(0).mediaSequence).isEqualTo(10);
assertThat(mediaPlaylists.get(0).segments).hasSize(4);
assertThat(mediaPlaylists.get(0).trailingParts).hasSize(1);
assertThat(mediaPlaylists.get(1).mediaSequence).isEqualTo(10);
assertThat(mediaPlaylists.get(1).segments).hasSize(4);
assertThat(mediaPlaylists.get(1).trailingParts).hasSize(2);
}
@Test
public void start_httpBadRequest_forcesFullNonBlockingPlaylistRequest()
throws IOException, TimeoutException, InterruptedException {
@ -263,9 +368,9 @@ public class DefaultHlsPlaylistTrackerTest {
new String[] {
"/master.m3u8",
"/media0/playlist.m3u8",
"/media0/playlist.m3u8?_HLS_skip=YES&_HLS_msn=16",
"/media0/playlist.m3u8?_HLS_msn=16&_HLS_skip=YES",
"/media0/playlist.m3u8",
"/media0/playlist.m3u8?_HLS_skip=YES&_HLS_msn=17"
"/media0/playlist.m3u8?_HLS_msn=17&_HLS_skip=YES"
},
getMockResponse(SAMPLE_M3U8_LIVE_MASTER),
getMockResponse(SAMPLE_M3U8_LIVE_MEDIA_CAN_SKIP_UNTIL_AND_BLOCK_RELOAD),

View File

@ -354,6 +354,7 @@ public class HlsMediaPlaylistParserTest {
assertThat(firstPart.durationUs).isEqualTo(2_000_000);
assertThat(firstPart.relativeStartTimeUs).isEqualTo(playlist.segments.get(0).durationUs);
assertThat(firstPart.isIndependent).isTrue();
assertThat(firstPart.isPreload).isFalse();
assertThat(firstPart.hasGapTag).isTrue();
assertThat(firstPart.url).isEqualTo("part267.1.ts");
HlsMediaPlaylist.Part secondPart = playlist.segments.get(1).parts.get(1);
@ -518,6 +519,7 @@ public class HlsMediaPlaylistParserTest {
assertThat(preloadPart.byteRangeOffset).isEqualTo(1234);
assertThat(preloadPart.initializationSegment.url).isEqualTo("map.mp4");
assertThat(preloadPart.encryptionIV).isEqualTo("0x410C8AC18AA42EFA18B5155484F5FC34");
assertThat(preloadPart.isPreload).isTrue();
}
@Test
@ -539,6 +541,7 @@ public class HlsMediaPlaylistParserTest {
assertThat(playlist.trailingParts).hasSize(2);
assertThat(playlist.trailingParts.get(1).url).isEqualTo("filePart267.2.ts");
assertThat(playlist.trailingParts.get(1).isPreload).isTrue();
}
@Test

View File

@ -0,0 +1,16 @@
#EXTM3U
#EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES
#EXT-X-TARGETDURATION:4
#EXT-X-PART-INF:PART-TARGET=1.000000
#EXT-X-VERSION:3
#EXT-X-MEDIA-SEQUENCE:10
#EXTINF:4.00000,
fileSequence10.ts
#EXTINF:4.00000,
fileSequence11.ts
#EXTINF:4.00000,
fileSequence12.ts
#EXTINF:4.00000,
fileSequence13.ts
#EXT-X-PART:DURATION=1.00000,URI="fileSequence14.0.ts"
#EXT-X-PRELOAD-HINT:TYPE=PART,URI="fileSequence14.1.ts"

View File

@ -0,0 +1,18 @@
#EXTM3U
#EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES
#EXT-X-TARGETDURATION:4
#EXT-X-PART-INF:PART-TARGET=1.000000
#EXT-X-VERSION:3
#EXT-X-MEDIA-SEQUENCE:10
#EXTINF:4.00000,
fileSequence10.ts
#EXTINF:4.00000,
fileSequence11.ts
#EXTINF:4.00000,
fileSequence12.ts
#EXT-X-PART:DURATION=1.00000,URI="fileSequence13.0.ts"
#EXT-X-PART:DURATION=1.00000,URI="fileSequence13.1.ts"
#EXT-X-PART:DURATION=1.00000,URI="fileSequence13.2.ts"
#EXT-X-PART:DURATION=1.00000,URI="fileSequence13.3.ts"
#EXTINF:4.00000,
fileSequence13.ts

View File

@ -0,0 +1,19 @@
#EXTM3U
#EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES
#EXT-X-TARGETDURATION:4
#EXT-X-PART-INF:PART-TARGET=1.000000
#EXT-X-VERSION:3
#EXT-X-MEDIA-SEQUENCE:10
#EXTINF:4.00000,
fileSequence10.ts
#EXTINF:4.00000,
fileSequence11.ts
#EXTINF:4.00000,
fileSequence12.ts
#EXT-X-PART:DURATION=1.00000,URI="fileSequence13.0.ts"
#EXT-X-PART:DURATION=1.00000,URI="fileSequence13.1.ts"
#EXT-X-PART:DURATION=1.00000,URI="fileSequence13.2.ts"
#EXT-X-PART:DURATION=1.00000,URI="fileSequence13.3.ts"
#EXTINF:4.00000,
fileSequence13.ts
#EXT-X-PART:DURATION=1.00000,URI="fileSequence14.0.ts"

View File

@ -0,0 +1,19 @@
#EXTM3U
#EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES
#EXT-X-TARGETDURATION:4
#EXT-X-PART-INF:PART-TARGET=1.000000
#EXT-X-VERSION:3
#EXT-X-MEDIA-SEQUENCE:10
#EXTINF:4.00000,
fileSequence10.ts
#EXTINF:4.00000,
fileSequence11.ts
#EXTINF:4.00000,
fileSequence12.ts
#EXT-X-PART:DURATION=1.00000,URI="fileSequence13.0.ts"
#EXT-X-PART:DURATION=1.00000,URI="fileSequence13.1.ts"
#EXT-X-PART:DURATION=1.00000,URI="fileSequence13.2.ts"
#EXT-X-PART:DURATION=1.00000,URI="fileSequence13.3.ts"
#EXTINF:4.00000,
fileSequence13.ts
#EXT-X-PRELOAD-HINT:TYPE=PART,URI="fileSequence14.0.ts"

View File

@ -0,0 +1,20 @@
#EXTM3U
#EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES
#EXT-X-TARGETDURATION:4
#EXT-X-PART-INF:PART-TARGET=1.000000
#EXT-X-VERSION:3
#EXT-X-MEDIA-SEQUENCE:10
#EXTINF:4.00000,
fileSequence10.ts
#EXTINF:4.00000,
fileSequence11.ts
#EXTINF:4.00000,
fileSequence12.ts
#EXT-X-PART:DURATION=1.00000,URI="fileSequence13.0.ts"
#EXT-X-PART:DURATION=1.00000,URI="fileSequence13.1.ts"
#EXT-X-PART:DURATION=1.00000,URI="fileSequence13.2.ts"
#EXT-X-PART:DURATION=1.00000,URI="fileSequence13.3.ts"
#EXTINF:4.00000,
fileSequence13.ts
#EXT-X-PART:DURATION=1.00000,URI="fileSequence14.0.ts"
#EXT-X-PRELOAD-HINT:TYPE=PART,URI="fileSequence14.1.ts"

View File

@ -0,0 +1,17 @@
#EXTM3U
#EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES
#EXT-X-TARGETDURATION:4
#EXT-X-PART-INF:PART-TARGET=1.000000
#EXT-X-VERSION:3
#EXT-X-MEDIA-SEQUENCE:10
#EXTINF:4.00000,
fileSequence10.ts
#EXTINF:4.00000,
fileSequence11.ts
#EXTINF:4.00000,
fileSequence12.ts
#EXTINF:4.00000,
fileSequence13.ts
#EXT-X-PART:DURATION=1.00000,URI="fileSequence14.0.ts"
#EXT-X-PART:DURATION=1.00000,URI="fileSequence14.1.ts"
#EXT-X-PRELOAD-HINT:TYPE=PART,URI="fileSequence14.2.ts"