Skip to content

Commit

Permalink
Make VNC recorded videos scrollable (#3180)
Browse files Browse the repository at this point in the history
This PR adds a second video recording mode, MP4, which is easier to consume and is scrollable.

Fixes #512

Co-authored-by: Kevin Wittek <kiview@users.noreply.github.com>
Co-authored-by: Sergei Egorov <segorov@vmware.com>
  • Loading branch information
3 people committed Feb 10, 2021
1 parent 51ee33b commit a6f91e3
Show file tree
Hide file tree
Showing 7 changed files with 194 additions and 34 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@

import lombok.Getter;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.ToString;
import org.apache.commons.compress.archivers.tar.TarArchiveInputStream;
import org.testcontainers.containers.wait.strategy.LogMessageWaitStrategy;
import org.testcontainers.utility.DockerImageName;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.StandardCopyOption;
Expand All @@ -25,34 +27,38 @@
@ToString
public class VncRecordingContainer extends GenericContainer<VncRecordingContainer> {

private static final String RECORDING_FILE_NAME = "/screen.flv";
private static final String ORIGINAL_RECORDING_FILE_NAME = "/screen.flv";

public static final String DEFAULT_VNC_PASSWORD = "secret";

public static final int DEFAULT_VNC_PORT = 5900;

static final VncRecordingFormat DEFAULT_RECORDING_FORMAT = VncRecordingFormat.FLV;

private final String targetNetworkAlias;

private String vncPassword = DEFAULT_VNC_PASSWORD;

private VncRecordingFormat videoFormat = DEFAULT_RECORDING_FORMAT;

private int vncPort = 5900;

private int frameRate = 30;

public VncRecordingContainer(@NonNull GenericContainer<?> targetContainer) {
this(
targetContainer.getNetwork(),
targetContainer.getNetworkAliases().stream()
.findFirst()
.orElseThrow(() -> new IllegalStateException("Target container must have a network alias"))
targetContainer.getNetwork(),
targetContainer.getNetworkAliases().stream()
.findFirst()
.orElseThrow(() -> new IllegalStateException("Target container must have a network alias"))
);
}

/**
* Create a sidekick container and attach it to another container. The VNC output of that container will be recorded.
*/
public VncRecordingContainer(@NonNull Network network, @NonNull String targetNetworkAlias) throws IllegalStateException {
super(DockerImageName.parse("testcontainers/vnc-recorder:1.1.0"));
super(DockerImageName.parse("testcontainers/vnc-recorder:1.2.0"));

this.targetNetworkAlias = targetNetworkAlias;
withNetwork(network);
Expand All @@ -71,6 +77,13 @@ public VncRecordingContainer withVncPort(int vncPort) {
return this;
}

public VncRecordingContainer withVideoFormat(VncRecordingFormat videoFormat) {
if (videoFormat != null) {
this.videoFormat = videoFormat;
}
return this;
}

public VncRecordingContainer withFrameRate(int frameRate) {
this.frameRate = frameRate;
return this;
Expand All @@ -81,25 +94,53 @@ protected void configure() {
withCreateContainerCmdModifier(it -> it.withEntrypoint("/bin/sh"));
String encodedPassword = Base64.getEncoder().encodeToString(vncPassword.getBytes());
setCommand(
"-c",
"echo '" + encodedPassword + "' | base64 -d > /vnc_password && " +
"flvrec.py -o " + RECORDING_FILE_NAME + " -d -r " + frameRate + " -P /vnc_password " + targetNetworkAlias + " " + vncPort
"-c",
"echo '" + encodedPassword + "' | base64 -d > /vnc_password && " +
"flvrec.py -o " + ORIGINAL_RECORDING_FILE_NAME + " -d -r " + frameRate + " -P /vnc_password " + targetNetworkAlias + " " + vncPort
);
}

@SneakyThrows
public InputStream streamRecording() {
String newRecordingFileName = videoFormat.reencodeRecording(this, ORIGINAL_RECORDING_FILE_NAME);

TarArchiveInputStream archiveInputStream = new TarArchiveInputStream(
dockerClient.copyArchiveFromContainerCmd(getContainerId(), RECORDING_FILE_NAME).exec()
dockerClient.copyArchiveFromContainerCmd(getContainerId(), newRecordingFileName).exec()
);
archiveInputStream.getNextEntry();
return archiveInputStream;
}

@SneakyThrows
public void saveRecordingToFile(File file) {
try(InputStream inputStream = streamRecording()) {
public void saveRecordingToFile(@NonNull File file) {
try (InputStream inputStream = streamRecording()) {
Files.copy(inputStream, file.toPath(), StandardCopyOption.REPLACE_EXISTING);
}
}

@RequiredArgsConstructor
public enum VncRecordingFormat {
FLV("flv") {
@Override
String reencodeRecording(@NonNull VncRecordingContainer container, @NonNull String source) throws IOException, InterruptedException {
String newFileOutput = "/newScreen.flv";
container.execInContainer("ffmpeg", "-i", source, "-vcodec", "libx264", newFileOutput);
return newFileOutput;
}
},
MP4("mp4") {
@Override
String reencodeRecording(@NonNull VncRecordingContainer container, @NonNull String source) throws IOException, InterruptedException {
String newFileOutput = "/newScreen.mp4";
container.execInContainer("ffmpeg", "-i", source, "-vcodec", "libx264", "-movflags", "faststart", newFileOutput);
return newFileOutput;
}
};

abstract String reencodeRecording(VncRecordingContainer container, String source) throws IOException, InterruptedException;

@Getter
private final String filenameExtension;
}

}
9 changes: 8 additions & 1 deletion docs/modules/webdriver_containers.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,14 @@ just for failing tests.
[Record failing Tests](../../modules/selenium/src/test/java/org/testcontainers/junit/ChromeRecordingWebDriverContainerTest.java) inside_block:recordFailing
<!--/codeinclude-->

Note that the seconds parameter to `withRecordingMode` should be a directory where recordings can be saved.
Note that the second parameter of `withRecordingMode` should be a directory where recordings can be saved.

By default, the video will be recorded in [FLV](https://en.wikipedia.org/wiki/Flash_Video) format, but you can specify it explicitly or change it to [MP4](https://en.wikipedia.org/wiki/MPEG-4_Part_14) using `withRecordingMode` method with `VncRecordingFormat` option:

<!--codeinclude-->
[Video Format in MP4](../../modules/selenium/src/test/java/org/testcontainers/junit/ChromeRecordingWebDriverContainerTest.java) inside_block:recordMp4
[Video Format in FLV](../../modules/selenium/src/test/java/org/testcontainers/junit/ChromeRecordingWebDriverContainerTest.java) inside_block:recordFlv
<!--/codeinclude-->

If you would like to customise the file name of the recording, or provide a different directory at runtime based on the description of the test and/or its success or failure, you may provide a custom recording file factory as follows:
<!--codeinclude-->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
import org.rnorth.ducttape.unreliables.Unreliables;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.testcontainers.containers.VncRecordingContainer.VncRecordingFormat;
import org.testcontainers.containers.traits.LinkableContainer;
import org.testcontainers.containers.wait.strategy.HostPortWaitStrategy;
import org.testcontainers.containers.wait.strategy.LogMessageWaitStrategy;
Expand Down Expand Up @@ -66,6 +67,7 @@ public class BrowserWebDriverContainer<SELF extends BrowserWebDriverContainer<SE
@Nullable
private RemoteWebDriver driver;
private VncRecordingMode recordingMode = VncRecordingMode.RECORD_FAILING;
private VncRecordingFormat recordingFormat;
private RecordingFileFactory recordingFileFactory;
private File vncRecordingDirectory;

Expand Down Expand Up @@ -182,7 +184,8 @@ protected void configure() {

vncRecordingContainer = new VncRecordingContainer(this)
.withVncPassword(DEFAULT_PASSWORD)
.withVncPort(VNC_PORT);
.withVncPort(VNC_PORT)
.withVideoFormat(recordingFormat);
}

if (customImageName != null) {
Expand Down Expand Up @@ -334,7 +337,7 @@ private void retainRecordingIfNeeded(String prefix, boolean succeeded) {
}

if (shouldRecord) {
File recordingFile = recordingFileFactory.recordingFileForTest(vncRecordingDirectory, prefix, succeeded);
File recordingFile = recordingFileFactory.recordingFileForTest(vncRecordingDirectory, prefix, succeeded, vncRecordingContainer.getVideoFormat());
LOGGER.info("Screen recordings for test {} will be stored at: {}", prefix, recordingFile);

vncRecordingContainer.saveRecordingToFile(recordingFile);
Expand All @@ -358,8 +361,13 @@ public SELF withLinkToContainer(LinkableContainer otherContainer, String alias)
}

public SELF withRecordingMode(VncRecordingMode recordingMode, File vncRecordingDirectory) {
return withRecordingMode(recordingMode, vncRecordingDirectory, null);
}

public SELF withRecordingMode(VncRecordingMode recordingMode, File vncRecordingDirectory, VncRecordingFormat recordingFormat) {
this.recordingMode = recordingMode;
this.vncRecordingDirectory = vncRecordingDirectory;
this.recordingFormat = recordingFormat;
return self();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,29 @@
import java.text.SimpleDateFormat;
import java.util.Date;

import static org.testcontainers.containers.VncRecordingContainer.VncRecordingFormat;

public class DefaultRecordingFileFactory implements RecordingFileFactory {

private static final SimpleDateFormat filenameDateFormat = new SimpleDateFormat("YYYYMMdd-HHmmss");
private static final String PASSED = "PASSED";
private static final String FAILED = "FAILED";
private static final String FILENAME_FORMAT = "%s-%s-%s.flv";
private static final String FILENAME_FORMAT = "%s-%s-%s.%s";


@Override
public File recordingFileForTest(File vncRecordingDirectory, String prefix, boolean succeeded) {
return recordingFileForTest(vncRecordingDirectory, prefix, succeeded, VncRecordingContainer.DEFAULT_RECORDING_FORMAT);
}

@Override
public File recordingFileForTest(File vncRecordingDirectory, String prefix, boolean succeeded, VncRecordingFormat recordingFormat) {
final String resultMarker = succeeded ? PASSED : FAILED;
final String fileName = String.format(FILENAME_FORMAT,
resultMarker,
prefix,
filenameDateFormat.format(new Date())
filenameDateFormat.format(new Date()),
recordingFormat.getFilenameExtension()
);
return new File(vncRecordingDirectory, fileName);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package org.testcontainers.containers;

import org.junit.runner.Description;
import org.testcontainers.containers.VncRecordingContainer.VncRecordingFormat;

import java.io.File;

Expand All @@ -11,5 +12,10 @@ default File recordingFileForTest(File vncRecordingDirectory, Description descri
return recordingFileForTest(vncRecordingDirectory, description.getTestClass().getSimpleName() + "-" + description.getMethodName(), succeeded);
}

default File recordingFileForTest(File vncRecordingDirectory, String prefix, boolean succeeded, VncRecordingFormat recordingFormat) {
return recordingFileForTest(vncRecordingDirectory, prefix, succeeded);
}

File recordingFileForTest(File vncRecordingDirectory, String prefix, boolean succeeded);

}
Original file line number Diff line number Diff line change
Expand Up @@ -57,4 +57,4 @@ public void recordingFileThatShouldDescribeTheTestResultAtThePresentTime() throw

assertThat(expectedPossibleFileNames, hasItem(recordingFile));
}
}
}

0 comments on commit a6f91e3

Please sign in to comment.