Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import com.comcast.viper.hlsparserj.tags.master.Media;
import com.comcast.viper.hlsparserj.tags.master.StreamInf;
import com.comcast.viper.hlsparserj.tags.media.ByteRange;
import com.comcast.viper.hlsparserj.tags.media.ExtInf;
import com.comcast.viper.hlsparserj.tags.media.ExtMap;
import com.comcast.viper.hlsparserj.tags.media.MediaSequence;
import com.comcast.viper.hlsparserj.tags.media.PlaylistType;
Expand Down Expand Up @@ -136,11 +137,34 @@ public List<MediaSegment> getMediaSegments() {
AtomicInteger sequenceNumber = new AtomicInteger(sequence);

if (!hasByteRange()) {
return mediaPlaylist.getSegments().stream()
.map(s -> new MediaSegment(sequenceNumber.getAndIncrement(),
uri.resolve(s.getURI()), Duration.ofMillis(Math.round(s.getDuration() * 1000))))
.collect(Collectors.toList());
// hlsparserj assigns the segment URI to the tag line immediately above it. When
// #EXT-X-PROGRAM-DATE-TIME (or other tags) sit between #EXTINF and the media URI
// line, ExtInf.getURI() is null. Playlists with PDT before #EXTINF parse correctly.
List<ExtInf> segments = mediaPlaylist.getSegments();
List<String> fallbackUris = requiresSegmentUriFallback(segments)
? extractSegmentUrisFromBody(body) : null;
List<MediaSegment> mediaSegments = new ArrayList<>(segments.size());
for (int i = 0; i < segments.size(); i++) {
ExtInf segment = segments.get(i);
String segmentUri = segment.getURI();
if (segmentUri == null && fallbackUris != null) {
if (i >= fallbackUris.size()) {
throw new IllegalStateException(
"Missing URI for segment at sequence " + sequenceNumber.get());
}
segmentUri = fallbackUris.get(i);
}
if (segmentUri == null) {
throw new IllegalStateException(
"Missing URI for segment at sequence " + sequenceNumber.get());
}
mediaSegments.add(new MediaSegment(sequenceNumber.getAndIncrement(),
uri.resolve(segmentUri), Duration.ofMillis(Math.round(segment.getDuration() * 1000))));
}
return mediaSegments;
} else {
// Byte-range playlists rely on hlsparserj associating URIs with EXT-X-BYTERANGE.
// Same PDT-between-EXTINF-and-URI ordering is not covered here
List<MediaSegment> mediaSegments = mediaPlaylist.getSegments().stream()
.map(s -> new MediaSegment(sequenceNumber.getAndIncrement(),
Duration.ofMillis(Math.round(s.getDuration() * 1000)))).collect(Collectors.toList());
Expand All @@ -156,6 +180,30 @@ public List<MediaSegment> getMediaSegments() {
}
}

static boolean requiresSegmentUriFallback(List<ExtInf> segments) {
return segments.stream().anyMatch(s -> s.getURI() == null);
}

static List<String> extractSegmentUrisFromBody(String body) {
List<String> uris = new ArrayList<>();
boolean afterExtInf = false;
for (String line : body.split("\n")) {
String trimmed = line.replace("\r", "").trim();
if (trimmed.isEmpty()) {
continue;
}
if (trimmed.startsWith("#EXTINF")) {
afterExtInf = true;
} else if (trimmed.startsWith("#")) {
continue;
} else if (afterExtInf) {
uris.add(trimmed);
afterExtInf = false;
}
}
return uris;
}

public long getTargetDurationSeconds() {
MediaPlaylist media = (MediaPlaylist) playlist;
TargetDuration td = media.getTargetDuration();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@
public class HlsSamplerTest extends VideoStreamingSamplerTest {

private static final String SIMPLE_MEDIA_PLAYLIST_NAME = "simpleMediaPlaylist.m3u8";
private static final String MEDIA_PLAYLIST_PDT_BETWEEN_EXTINF_AND_URI_NAME =
"mediaPlaylistPdtBetweenExtInfAndUri.m3u8";
private static final double PDT_BETWEEN_EXTINF_AND_URI_SEGMENT_DURATION = 4.0;
private static final URI MASTER_URI = URI.create(BASE_URI + "/master.m3u8");
protected static final String MEDIA_TYPE_NAME = "media";
protected static final double MEDIA_SEGMENT_DURATION = 5.0;
Expand Down Expand Up @@ -95,6 +98,18 @@ public void shouldDownloadSegmentWhenUriIsFromMediaPlaylist() throws Exception {
buildMediaSegmentSampleResult(1));
}

@Test
public void shouldDownloadSegmentWhenPdtIsBetweenExtInfAndUri() throws Exception {
String mediaPlaylist = getResource(MEDIA_PLAYLIST_PDT_BETWEEN_EXTINF_AND_URI_NAME);
setupUriSamplerPlaylist(MASTER_URI, mediaPlaylist);
setPlaySeconds(PDT_BETWEEN_EXTINF_AND_URI_SEGMENT_DURATION);
sampler.sample();
verifySampleResults(
buildBaseSampleResult(MEDIA_PLAYLIST_SAMPLE_NAME, MASTER_URI, mediaPlaylist),
buildMediaSegmentSampleResult(MASTER_URI.resolve("segment_001.ts"),
PDT_BETWEEN_EXTINF_AND_URI_SEGMENT_DURATION));
}

private void setupUriSamplerPlaylist(URI uri, String... playlists) {
HTTPSampleResult[] rest = new HTTPSampleResult[playlists.length - 1];
for (int i = 1; i < playlists.length; i++) {
Expand All @@ -109,6 +124,12 @@ private HTTPSampleResult buildMediaSegmentSampleResult(int sequenceNumber) {
MEDIA_SEGMENT_DURATION);
}

private HTTPSampleResult buildMediaSegmentSampleResult(URI segmentUri, double segmentDuration) {
HTTPSampleResult result = buildSampleResult(segmentUri, SEGMENT_CONTENT_TYPE, "");
result.setSampleLabel(buildSegmentName(MEDIA_TYPE_NAME));
return addDurationHeader(result, segmentDuration);
}

private HTTPSampleResult buildExpectedSegmentSampleResult(int sequenceNumber, String segmentType,
double segmentDuration) {
HTTPSampleResult result = buildBaseSegmentSampleResult(segmentType, sequenceNumber);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package com.blazemeter.jmeter.videostreaming.hls;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;

import com.blazemeter.jmeter.videostreaming.core.MediaSegment;
import com.blazemeter.jmeter.videostreaming.core.exception.PlaylistParsingException;
import com.comcast.viper.hlsparserj.MediaPlaylist;
import com.comcast.viper.hlsparserj.tags.media.ExtInf;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.time.Instant;
import java.util.List;
import org.apache.commons.io.IOUtils;
import org.junit.Test;

public class PlaylistTest {

private static final URI MEDIA_PLAYLIST_URI = URI.create("http://test/media/variant.m3u8");

@Test
public void shouldResolveSegmentUrisWhenPdtIsBetweenExtInfAndUri() throws Exception {
Playlist playlist = parseResource("mediaPlaylistPdtBetweenExtInfAndUri.m3u8");
List<ExtInf> parsedSegments = ((MediaPlaylist) playlist.getPlaylist()).getSegments();

assertTrue(Playlist.requiresSegmentUriFallback(parsedSegments));

List<MediaSegment> segments = playlist.getMediaSegments();

assertEquals(3, segments.size());
assertEquals(MEDIA_PLAYLIST_URI.resolve("segment_001.ts"), segments.get(0).getUri());
assertEquals(MEDIA_PLAYLIST_URI.resolve("segment_002.ts"), segments.get(1).getUri());
assertEquals(MEDIA_PLAYLIST_URI.resolve("segment_003.ts"), segments.get(2).getUri());
assertEquals(17456L, segments.get(0).getSequenceNumber());
assertEquals(17457L, segments.get(1).getSequenceNumber());
assertEquals(17458L, segments.get(2).getSequenceNumber());
}

@Test
public void shouldKeepParserSegmentUrisWhenPdtIsBeforeExtInf() throws Exception {
Playlist playlist = parseResource("mediaPlaylistPdtBeforeExtInf.m3u8");
List<ExtInf> parsedSegments = ((MediaPlaylist) playlist.getPlaylist()).getSegments();

assertFalse(Playlist.requiresSegmentUriFallback(parsedSegments));

List<MediaSegment> segments = playlist.getMediaSegments();

assertEquals(2, segments.size());
assertEquals(MEDIA_PLAYLIST_URI.resolve("segment_001.ts"), segments.get(0).getUri());
assertEquals(MEDIA_PLAYLIST_URI.resolve("segment_002.ts"), segments.get(1).getUri());
}

@Test
public void shouldUseParserSegmentUrisWithoutFallbackForStandardPlaylist() throws Exception {
Playlist playlist = parseResource("simpleMediaPlaylist.m3u8");
List<ExtInf> parsedSegments = ((MediaPlaylist) playlist.getPlaylist()).getSegments();

assertFalse(Playlist.requiresSegmentUriFallback(parsedSegments));

List<MediaSegment> segments = playlist.getMediaSegments();

assertEquals(3, segments.size());
assertEquals(MEDIA_PLAYLIST_URI.resolve("/media/001.ts"), segments.get(0).getUri());
assertEquals(MEDIA_PLAYLIST_URI.resolve("/media/002.ts"), segments.get(1).getUri());
assertEquals(MEDIA_PLAYLIST_URI.resolve("/media/003.ts"), segments.get(2).getUri());
}

private static Playlist parseResource(String resourceName)
throws IOException, PlaylistParsingException {
String path = "com/blazemeter/jmeter/videostreaming/hls/" + resourceName;
try (InputStream in = PlaylistTest.class.getClassLoader().getResourceAsStream(path)) {
String body = IOUtils.toString(in, StandardCharsets.UTF_8);
return Playlist.fromUriAndBody(MEDIA_PLAYLIST_URI, body, Instant.now());
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
#EXTM3U
#EXT-X-VERSION:6
#EXT-X-TARGETDURATION:4
#EXT-X-MEDIA-SEQUENCE:100
#EXT-X-PROGRAM-DATE-TIME:2026-06-02T16:33:11.352+0200
#EXTINF:4,
segment_001.ts
#EXT-X-PROGRAM-DATE-TIME:2026-06-02T16:33:15.352+0200
#EXTINF:4,
segment_002.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
#EXTM3U
#EXT-X-VERSION:6
#EXT-X-TARGETDURATION:4
#EXT-X-MEDIA-SEQUENCE:17456
#EXT-X-INDEPENDENT-SEGMENTS
#EXTINF:4,
#EXT-X-PROGRAM-DATE-TIME:2026-06-02T16:33:11.352+0200
segment_001.ts
#EXTINF:4,
#EXT-X-PROGRAM-DATE-TIME:2026-06-02T16:33:15.352+0200
segment_002.ts
#EXTINF:4,
#EXT-X-PROGRAM-DATE-TIME:2026-06-02T16:33:19.352+0200
segment_003.ts