Skip to content

Commit

Permalink
Fix master/slave issue & URL recognition
Browse files Browse the repository at this point in the history
• Due to RMI usage of JMeter when running a master/slave test, users were experimenting a NullPointerException due to bad object serialization.
• Improvement on URL recognition. IMPORTANT: For HLS protocol test (not MPG-DASH) is mandatory that the URL contains its extension ".m3u8" as is explained on ISO regulation: https://tools.ietf.org/html/rfc8216#section-4
  • Loading branch information
Baraujo25 committed Jul 28, 2020
1 parent cc17d8b commit 5006c6f
Show file tree
Hide file tree
Showing 14 changed files with 438 additions and 20 deletions.
12 changes: 8 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,26 +6,30 @@ Likewise, trying to provide a wider spectrum of protocols to support videos stre

For more information related to HLS, please refer to the [wikipedia page](https://en.wikipedia.org/wiki/HTTP_Live_Streaming) or to the [RFC](https://tools.ietf.org/html/rfc8216) and, for MPEG DASH, please refer to the [wikipedia page](https://en.wikipedia.org/wiki/Dynamic_Adaptive_Streaming_over_HTTP) or to the [ISO](https://standards.iso.org/ittf/PubliclyAvailableStandards/c065274_ISO_IEC_23009-1_2014.zip).

Currently the project uses the [HLSParserJ](https://github.com/Comcast/hlsparserj) library to parse the HLS playlists and a [fork](https://github.com/Blazemeter/mpd-tools) of [MPD-Tools](https://github.com/carlanton/mpd-tools) for MPEG-DASH manifest and segments.
Currently, the project uses the [HLSParserJ](https://github.com/Comcast/hlsparserj) library to parse the HLS playlists and a [fork](https://github.com/Blazemeter/mpd-tools) of [MPD-Tools](https://github.com/carlanton/mpd-tools) for MPEG-DASH manifest and segments.

**NOTICE**

In future releases, the plugin will be named "Video Streaming Plugin" instead of "HLS Plugin", following the same desire to cover a wider range of protocols.

#### In a HTTP Live Streaming process:
#### In an HTTP Live Streaming process:

- The audio/video to be streamed is reproduced by a media encoder at different quality levels, bitrates and resolutions. Each version is called a variant.
- The different variants are split up into smaller Media Segment Files.
- The encoder creates a Media Playlist for each variant with the URLs of each Media Segment.
- The encoder creates a Master Playlist File with the URLs of each Media Playlist.
To play, the client first downloads the Master Playlist, and then the Media Playlists. Then, they play each Media Segment declared within the chosen Media Playlist. The client can reload the Playlist to discover any added segments. This is needed in cases of live events, for example.

Notice that the recognition of the HLS protocol is based on the requirement of the URL extension of the Master playlist link, which must have ".m3u8" on it, as specified on the [ISO regulation](https://tools.ietf.org/html/rfc8216#section-4).

#### In a Dynamic Adaptive Streaming over HTTP Live Streaming process:

- The encoder creates a Manifest that contains all the Periods, among Base URLs and the Adaptation Sets to do the filtering, based on resolution, bandwidth and language selector.
- The encoder creates a Manifest which contains all the Periods, among Base URLs and the Adaptation Sets to do the filtering, based on resolution, bandwidth and language selector.
- The plugin is coded so it will download the segments, for each Adaptation Set selected, consecutively, instead of doing it in parallel.
- The plugin will update the manifest based on the ```timeShiftBufferDepth``` attribute of MPD.

Notice that, just like is done for HLS, the recognition on this protocol is based on the URL of the Manifest, which should contain ".mpd" on it. In cases, it doesn't meet this requirement, and the url don't contain ".m3a8", it is going to be considered a MPEG-DASH as well.

## How the plugin works

### Concept
Expand Down Expand Up @@ -59,7 +63,7 @@ Set the link to the master playlist file

#### Duration

Set the playback time to either the whole video or a certain amount of seconds.
Set the playback time to either the whole video, or a certain amount of seconds.

![](docs/duration.png)

Expand Down
17 changes: 16 additions & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

<groupId>com.blazemeter.jmeter</groupId>
<artifactId>jmeter-bzm-hls</artifactId>
<version>3.0.1-SNAPSHOT</version>
<version>3.0.3</version>
<name>Video Streaming Sampler as JMeter plugin</name>

<properties>
Expand Down Expand Up @@ -67,6 +67,12 @@
<version>4.12</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>testcontainers</artifactId>
<version>1.14.3</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>kg.apc</groupId>
<artifactId>jmeter-plugins-cmn-jmeter</artifactId>
Expand Down Expand Up @@ -103,6 +109,12 @@
<version>3.8.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.github.tomakehurst</groupId>
<artifactId>wiremock</artifactId>
<version>2.27.1</version>
<scope>test</scope>
</dependency>
</dependencies>

<build>
Expand Down Expand Up @@ -147,6 +159,9 @@
<parallel>both</parallel>
<threadCount>10</threadCount>
<useSystemClassLoader>false</useSystemClassLoader>
<excludedGroups>
com.blazemeter.jmeter.MasterSlaveIT
</excludedGroups>
</configuration>
</plugin>
</plugins>
Expand Down
49 changes: 34 additions & 15 deletions src/main/java/com/blazemeter/jmeter/hls/logic/HlsSampler.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@
import com.blazemeter.jmeter.videostreaming.core.TimeMachine;
import com.blazemeter.jmeter.videostreaming.core.VideoStreamingHttpClient;
import com.blazemeter.jmeter.videostreaming.core.VideoStreamingSampler;
import com.blazemeter.jmeter.videostreaming.dash.DashSampler;
import com.helger.commons.annotation.VisibleForTesting;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.net.URL;
import org.apache.jmeter.engine.event.LoopIterationEvent;
import org.apache.jmeter.protocol.http.control.CacheManager;
Expand Down Expand Up @@ -43,29 +45,45 @@ public class HlsSampler extends HTTPSamplerBase implements Interruptible {
private static final String COOKIE_MANAGER = "HLSRequest.cookie_manager";
private static final String CACHE_MANAGER = "HLSRequest.cache_manager";

private final transient VideoStreamingHttpClient httpClient;
private final transient TimeMachine timeMachine;
private final transient SampleResultProcessor sampleResultProcessor;
private transient VideoStreamingHttpClient httpClient;
private transient TimeMachine timeMachine;
private transient SampleResultProcessor sampleResultProcessor;
private transient VideoStreamingSampler<?, ?> sampler;

private transient String lastMasterUrl = null;
private transient volatile boolean notifyFirstSampleAfterLoopRestart;
private transient VideoStreamingSamplerFactory factory;

public HlsSampler() {
initHttpSampler();
httpClient = new VideoStreamingHttpClient(this);
sampleResultProcessor = new SampleResultProcessor(this);
timeMachine = TimeMachine.SYSTEM;
}

@VisibleForTesting
public HlsSampler(VideoStreamingSamplerFactory factory, VideoStreamingHttpClient client,
SampleResultProcessor processor, TimeMachine timeMachine) {
this.factory = factory;
this.httpClient = client;
this.sampleResultProcessor = processor;
this.timeMachine = timeMachine;
}

public HlsSampler(VideoStreamingHttpClient httpClient, TimeMachine timeMachine) {
initHttpSampler();
setInitHttpSamplerConfig();
this.httpClient = httpClient;
sampleResultProcessor = new SampleResultProcessor(this);
this.timeMachine = timeMachine;
sampleResultProcessor = new SampleResultProcessor(this);
factory = new VideoStreamingSamplerFactory();
}

private void initHttpSampler() {
setInitHttpSamplerConfig();
factory = new VideoStreamingSamplerFactory();
httpClient = new VideoStreamingHttpClient(this);
sampleResultProcessor = new SampleResultProcessor(this);
timeMachine = TimeMachine.SYSTEM;
}

private void setInitHttpSamplerConfig() {
setName("Media Sampler");
setFollowRedirects(true);
setUseKeepAlive(true);
Expand Down Expand Up @@ -171,12 +189,8 @@ public SampleResult sample() {

String url = getMasterUrl();
if (!url.equals(lastMasterUrl)) {
if (!url.contains(".mpd")) {
sampler = new com.blazemeter.jmeter.videostreaming.hls.HlsSampler(this, httpClient,
timeMachine, sampleResultProcessor);
} else {
sampler = new DashSampler(this, httpClient, timeMachine, sampleResultProcessor);
}
sampler = factory
.getVideoStreamingSampler(url, this, httpClient, timeMachine, sampleResultProcessor);
} else if (!this.getResumeVideoStatus()) {
sampler.resetVideoStatus();
}
Expand Down Expand Up @@ -234,4 +248,9 @@ public void testStarted() {
timeMachine.reset();
}

private void readObject(ObjectInputStream inputStream)
throws IOException, ClassNotFoundException {
inputStream.defaultReadObject();
initHttpSampler();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package com.blazemeter.jmeter.hls.logic;

import com.blazemeter.jmeter.videostreaming.core.SampleResultProcessor;
import com.blazemeter.jmeter.videostreaming.core.TimeMachine;
import com.blazemeter.jmeter.videostreaming.core.VideoStreamingHttpClient;
import com.blazemeter.jmeter.videostreaming.core.VideoStreamingSampler;
import com.blazemeter.jmeter.videostreaming.dash.DashSampler;

public class VideoStreamingSamplerFactory {

public VideoStreamingSampler<?, ?> getVideoStreamingSampler(String url, HlsSampler baseSampler,
VideoStreamingHttpClient httpClient,
TimeMachine timeMachine, SampleResultProcessor sampleResultProcessor) {
//HLS Master Playlist must contain this .m3u8 extension in their URLs
if (url.contains(".m3u8")) {
return createHlsSampler(baseSampler, httpClient, timeMachine, sampleResultProcessor);
} else {
return createDashSampler(baseSampler, httpClient, timeMachine, sampleResultProcessor);
}
}

private DashSampler createDashSampler(HlsSampler baseSampler, VideoStreamingHttpClient httpClient,
TimeMachine timeMachine, SampleResultProcessor sampleResultProcessor) {
return new DashSampler(baseSampler, httpClient, timeMachine, sampleResultProcessor);
}

private com.blazemeter.jmeter.videostreaming.hls.HlsSampler createHlsSampler(
HlsSampler baseSampler, VideoStreamingHttpClient httpClient, TimeMachine timeMachine,
SampleResultProcessor sampleResultProcessor) {
return new com.blazemeter.jmeter.videostreaming.hls.HlsSampler(baseSampler, httpClient,
timeMachine, sampleResultProcessor);
}
}
84 changes: 84 additions & 0 deletions src/test/java/com/blazemeter/jmeter/MasterSlaveIT.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package com.blazemeter.jmeter;

import static org.assertj.core.api.AssertionsForClassTypes.assertThat;

import java.io.File;
import java.io.IOException;
import java.time.Duration;
import org.junit.Rule;
import org.junit.Test;
import org.junit.experimental.categories.Category;
import org.junit.rules.RuleChain;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.testcontainers.containers.Container.ExecResult;
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.containers.Network;
import org.testcontainers.containers.output.Slf4jLogConsumer;
import org.testcontainers.containers.wait.strategy.LogMessageWaitStrategy;
import org.testcontainers.images.builder.ImageFromDockerfile;

@Category(MasterSlaveIT.class)
public class MasterSlaveIT {

private static final Logger LOG = LoggerFactory.getLogger(MasterSlaveIT.class);
private static final long TIMEOUT_MILLI = 120000;
private static final String JMETER_HOME_PATH = "/jmeter/apache-jmeter-5.1.1/bin";
private static final Network network = Network.newNetwork();

public GenericContainer<?> wiremockContainer;
public GenericContainer<?> container;

@Rule
public RuleChain chain = RuleChain
.outerRule(wiremockContainer = buildWiremockContainerFromDockerfile())
.around(container = getJavaContainerFromDockerfile());

@Test(timeout = TIMEOUT_MILLI * 2)
public void shouldRunHLSMasterSlaveTestWhenStartContainer()
throws IOException, InterruptedException {
ExecResult execResult = container.execInContainer("sh", JMETER_HOME_PATH + "/jmeter",
"-n", "-r", "-t", "/test.jmx", "-l", "/result", "-j", "/master_logs");

assertThat(execResult.getStdout()).contains("... end of run");
}

private GenericContainer<?> buildWiremockContainerFromDockerfile() {
return new GenericContainer<>(
new ImageFromDockerfile()
//adding files to test-container context
.withFileFromClasspath("mapping.json", "master-slave/mapping.json")
.withFileFromClasspath("Dockerfile", "master-slave/WiremockDockerfile")
.withDockerfilePath("Dockerfile"))
.withLogConsumer(new Slf4jLogConsumer(LOG).withPrefix("WIREMOCK"))
.withExposedPorts(8080)
.withNetwork(network)
.withNetworkAliases("wiremock")
// wait for wiremock to be running
.waitingFor(new LogMessageWaitStrategy()
.withRegEx(".*(/\\$\\$ /\\$\\$ /\\$\\$ "
+ " /\\$\\$ /\\$\\$ /\\$\\$ ).*")
.withStartupTimeout(Duration.ofMillis(TIMEOUT_MILLI)));
}

private GenericContainer<?> getJavaContainerFromDockerfile() {
return new GenericContainer<>(
new ImageFromDockerfile()
//adding files to test-container context
.withFileFromClasspath("master-slave-test.sh", "master-slave/master-slave-test.sh")
.withFileFromClasspath("Dockerfile", "master-slave/Dockerfile")
.withFileFromClasspath("test.jmx", "master-slave/HLSSamplerSlaveRemoteTest.jmx")
.withFileFromFile("jmeter-bzm-hls.jar",
new File("target/jmeter-test/lib/jmeter-bzm-hls.jar"))
.withFileFromFile("hlsparserj.jar",
new File("target/jmeter-test/lib/hlsparserj.jar"))
.withDockerfilePath("Dockerfile"))
.withLogConsumer(new Slf4jLogConsumer(LOG).withPrefix("MAIN"))
.withNetwork(network)
.withNetworkAliases("master")
.waitingFor(new LogMessageWaitStrategy().withRegEx(".*Created\\sremote\\sobject.*")
.withStartupTimeout(Duration.ofMillis(TIMEOUT_MILLI)));

}

}
53 changes: 53 additions & 0 deletions src/test/java/com/blazemeter/jmeter/hls/logic/HlsSamplerTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package com.blazemeter.jmeter.hls.logic;

import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.only;
import static org.mockito.Mockito.verify;

import com.blazemeter.jmeter.videostreaming.core.MediaSegment;
import com.blazemeter.jmeter.videostreaming.core.SampleResultProcessor;
import com.blazemeter.jmeter.videostreaming.core.TimeMachine;
import com.blazemeter.jmeter.videostreaming.core.VideoStreamingHttpClient;
import com.blazemeter.jmeter.videostreaming.core.VideoStreamingSampler;
import com.blazemeter.jmeter.videostreaming.hls.Playlist;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;

@RunWith(MockitoJUnitRunner.class)
public class HlsSamplerTest {

@Mock
private VideoStreamingSampler<Playlist, MediaSegment> videoStreamingSampler;
@Mock
private VideoStreamingSamplerFactory factory;
@Mock
private VideoStreamingHttpClient client;
@Mock
private TimeMachine timeMachine;
@Mock
private SampleResultProcessor processor;

private HlsSampler hlsSampler;

@Before
public void setUp() {
hlsSampler = new HlsSampler(factory, client, processor, timeMachine);
}

@Test
public void shouldFactoryGetVideoStreamingSamplerWhenSample() {
String masterUrl = "hls_master_playlist.m3u8";
hlsSampler.setMasterUrl(masterUrl);

doReturn(videoStreamingSampler)
.when(factory)
.getVideoStreamingSampler(masterUrl, hlsSampler, client, timeMachine, processor);

hlsSampler.sample();
verify(factory, only())
.getVideoStreamingSampler(masterUrl, hlsSampler, client, timeMachine, processor);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package com.blazemeter.jmeter.hls.logic;

import static org.assertj.core.api.Assertions.assertThat;

import com.blazemeter.jmeter.videostreaming.core.SampleResultProcessor;
import com.blazemeter.jmeter.videostreaming.core.TimeMachine;
import com.blazemeter.jmeter.videostreaming.core.VideoStreamingHttpClient;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mock;

public class VideoStreamingSamplerFactoryTest {

@Mock
private HlsSampler proxy;
@Mock
private VideoStreamingHttpClient client;
@Mock
private TimeMachine timeMachine;
@Mock
private SampleResultProcessor processor;

private VideoStreamingSamplerFactory factory;

@Before
public void setUp() {
factory = new VideoStreamingSamplerFactory();
}

@Test
public void shouldCreateHlsSamplerWhenUrlContainsExtension() {
assertThat(factory
.getVideoStreamingSampler("test.com/master.m3u8", proxy, client, timeMachine, processor))
.isInstanceOf(com.blazemeter.jmeter.videostreaming.hls.HlsSampler.class);
}

@Test
public void shouldNotCreateHlsSamplerWhenUrlNotContainsExtension() {
assertThat(factory
.getVideoStreamingSampler("test.com/master.mpd", proxy, client, timeMachine, processor))
.isNotInstanceOf(com.blazemeter.jmeter.videostreaming.hls.HlsSampler.class);
}
}
Loading

0 comments on commit 5006c6f

Please sign in to comment.