Skip to content

Commit a6f91e3

Browse files
oussamabadrkiviewbsideup
authored
Make VNC recorded videos scrollable (#3180)
This PR adds a second video recording mode, MP4, which is easier to consume and is scrollable. Fixes #512 Co-authored-by: Kevin Wittek <[email protected]> Co-authored-by: Sergei Egorov <[email protected]>
1 parent 51ee33b commit a6f91e3

File tree

7 files changed

+194
-34
lines changed

7 files changed

+194
-34
lines changed

core/src/main/java/org/testcontainers/containers/VncRecordingContainer.java

Lines changed: 53 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,15 @@
22

33
import lombok.Getter;
44
import lombok.NonNull;
5+
import lombok.RequiredArgsConstructor;
56
import lombok.SneakyThrows;
67
import lombok.ToString;
78
import org.apache.commons.compress.archivers.tar.TarArchiveInputStream;
89
import org.testcontainers.containers.wait.strategy.LogMessageWaitStrategy;
910
import org.testcontainers.utility.DockerImageName;
1011

1112
import java.io.File;
13+
import java.io.IOException;
1214
import java.io.InputStream;
1315
import java.nio.file.Files;
1416
import java.nio.file.StandardCopyOption;
@@ -25,34 +27,38 @@
2527
@ToString
2628
public class VncRecordingContainer extends GenericContainer<VncRecordingContainer> {
2729

28-
private static final String RECORDING_FILE_NAME = "/screen.flv";
30+
private static final String ORIGINAL_RECORDING_FILE_NAME = "/screen.flv";
2931

3032
public static final String DEFAULT_VNC_PASSWORD = "secret";
3133

3234
public static final int DEFAULT_VNC_PORT = 5900;
3335

36+
static final VncRecordingFormat DEFAULT_RECORDING_FORMAT = VncRecordingFormat.FLV;
37+
3438
private final String targetNetworkAlias;
3539

3640
private String vncPassword = DEFAULT_VNC_PASSWORD;
3741

42+
private VncRecordingFormat videoFormat = DEFAULT_RECORDING_FORMAT;
43+
3844
private int vncPort = 5900;
3945

4046
private int frameRate = 30;
4147

4248
public VncRecordingContainer(@NonNull GenericContainer<?> targetContainer) {
4349
this(
44-
targetContainer.getNetwork(),
45-
targetContainer.getNetworkAliases().stream()
46-
.findFirst()
47-
.orElseThrow(() -> new IllegalStateException("Target container must have a network alias"))
50+
targetContainer.getNetwork(),
51+
targetContainer.getNetworkAliases().stream()
52+
.findFirst()
53+
.orElseThrow(() -> new IllegalStateException("Target container must have a network alias"))
4854
);
4955
}
5056

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

5763
this.targetNetworkAlias = targetNetworkAlias;
5864
withNetwork(network);
@@ -71,6 +77,13 @@ public VncRecordingContainer withVncPort(int vncPort) {
7177
return this;
7278
}
7379

80+
public VncRecordingContainer withVideoFormat(VncRecordingFormat videoFormat) {
81+
if (videoFormat != null) {
82+
this.videoFormat = videoFormat;
83+
}
84+
return this;
85+
}
86+
7487
public VncRecordingContainer withFrameRate(int frameRate) {
7588
this.frameRate = frameRate;
7689
return this;
@@ -81,25 +94,53 @@ protected void configure() {
8194
withCreateContainerCmdModifier(it -> it.withEntrypoint("/bin/sh"));
8295
String encodedPassword = Base64.getEncoder().encodeToString(vncPassword.getBytes());
8396
setCommand(
84-
"-c",
85-
"echo '" + encodedPassword + "' | base64 -d > /vnc_password && " +
86-
"flvrec.py -o " + RECORDING_FILE_NAME + " -d -r " + frameRate + " -P /vnc_password " + targetNetworkAlias + " " + vncPort
97+
"-c",
98+
"echo '" + encodedPassword + "' | base64 -d > /vnc_password && " +
99+
"flvrec.py -o " + ORIGINAL_RECORDING_FILE_NAME + " -d -r " + frameRate + " -P /vnc_password " + targetNetworkAlias + " " + vncPort
87100
);
88101
}
89102

90103
@SneakyThrows
91104
public InputStream streamRecording() {
105+
String newRecordingFileName = videoFormat.reencodeRecording(this, ORIGINAL_RECORDING_FILE_NAME);
106+
92107
TarArchiveInputStream archiveInputStream = new TarArchiveInputStream(
93-
dockerClient.copyArchiveFromContainerCmd(getContainerId(), RECORDING_FILE_NAME).exec()
108+
dockerClient.copyArchiveFromContainerCmd(getContainerId(), newRecordingFileName).exec()
94109
);
95110
archiveInputStream.getNextEntry();
96111
return archiveInputStream;
97112
}
98113

99114
@SneakyThrows
100-
public void saveRecordingToFile(File file) {
101-
try(InputStream inputStream = streamRecording()) {
115+
public void saveRecordingToFile(@NonNull File file) {
116+
try (InputStream inputStream = streamRecording()) {
102117
Files.copy(inputStream, file.toPath(), StandardCopyOption.REPLACE_EXISTING);
103118
}
104119
}
120+
121+
@RequiredArgsConstructor
122+
public enum VncRecordingFormat {
123+
FLV("flv") {
124+
@Override
125+
String reencodeRecording(@NonNull VncRecordingContainer container, @NonNull String source) throws IOException, InterruptedException {
126+
String newFileOutput = "/newScreen.flv";
127+
container.execInContainer("ffmpeg", "-i", source, "-vcodec", "libx264", newFileOutput);
128+
return newFileOutput;
129+
}
130+
},
131+
MP4("mp4") {
132+
@Override
133+
String reencodeRecording(@NonNull VncRecordingContainer container, @NonNull String source) throws IOException, InterruptedException {
134+
String newFileOutput = "/newScreen.mp4";
135+
container.execInContainer("ffmpeg", "-i", source, "-vcodec", "libx264", "-movflags", "faststart", newFileOutput);
136+
return newFileOutput;
137+
}
138+
};
139+
140+
abstract String reencodeRecording(VncRecordingContainer container, String source) throws IOException, InterruptedException;
141+
142+
@Getter
143+
private final String filenameExtension;
144+
}
145+
105146
}

docs/modules/webdriver_containers.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,14 @@ just for failing tests.
6363
[Record failing Tests](../../modules/selenium/src/test/java/org/testcontainers/junit/ChromeRecordingWebDriverContainerTest.java) inside_block:recordFailing
6464
<!--/codeinclude-->
6565

66-
Note that the seconds parameter to `withRecordingMode` should be a directory where recordings can be saved.
66+
Note that the second parameter of `withRecordingMode` should be a directory where recordings can be saved.
67+
68+
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:
69+
70+
<!--codeinclude-->
71+
[Video Format in MP4](../../modules/selenium/src/test/java/org/testcontainers/junit/ChromeRecordingWebDriverContainerTest.java) inside_block:recordMp4
72+
[Video Format in FLV](../../modules/selenium/src/test/java/org/testcontainers/junit/ChromeRecordingWebDriverContainerTest.java) inside_block:recordFlv
73+
<!--/codeinclude-->
6774

6875
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:
6976
<!--codeinclude-->

modules/selenium/src/main/java/org/testcontainers/containers/BrowserWebDriverContainer.java

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
import org.rnorth.ducttape.unreliables.Unreliables;
2828
import org.slf4j.Logger;
2929
import org.slf4j.LoggerFactory;
30+
import org.testcontainers.containers.VncRecordingContainer.VncRecordingFormat;
3031
import org.testcontainers.containers.traits.LinkableContainer;
3132
import org.testcontainers.containers.wait.strategy.HostPortWaitStrategy;
3233
import org.testcontainers.containers.wait.strategy.LogMessageWaitStrategy;
@@ -66,6 +67,7 @@ public class BrowserWebDriverContainer<SELF extends BrowserWebDriverContainer<SE
6667
@Nullable
6768
private RemoteWebDriver driver;
6869
private VncRecordingMode recordingMode = VncRecordingMode.RECORD_FAILING;
70+
private VncRecordingFormat recordingFormat;
6971
private RecordingFileFactory recordingFileFactory;
7072
private File vncRecordingDirectory;
7173

@@ -182,7 +184,8 @@ protected void configure() {
182184

183185
vncRecordingContainer = new VncRecordingContainer(this)
184186
.withVncPassword(DEFAULT_PASSWORD)
185-
.withVncPort(VNC_PORT);
187+
.withVncPort(VNC_PORT)
188+
.withVideoFormat(recordingFormat);
186189
}
187190

188191
if (customImageName != null) {
@@ -334,7 +337,7 @@ private void retainRecordingIfNeeded(String prefix, boolean succeeded) {
334337
}
335338

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

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

360363
public SELF withRecordingMode(VncRecordingMode recordingMode, File vncRecordingDirectory) {
364+
return withRecordingMode(recordingMode, vncRecordingDirectory, null);
365+
}
366+
367+
public SELF withRecordingMode(VncRecordingMode recordingMode, File vncRecordingDirectory, VncRecordingFormat recordingFormat) {
361368
this.recordingMode = recordingMode;
362369
this.vncRecordingDirectory = vncRecordingDirectory;
370+
this.recordingFormat = recordingFormat;
363371
return self();
364372
}
365373

modules/selenium/src/main/java/org/testcontainers/containers/DefaultRecordingFileFactory.java

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,20 +4,29 @@
44
import java.text.SimpleDateFormat;
55
import java.util.Date;
66

7+
import static org.testcontainers.containers.VncRecordingContainer.VncRecordingFormat;
8+
79
public class DefaultRecordingFileFactory implements RecordingFileFactory {
810

911
private static final SimpleDateFormat filenameDateFormat = new SimpleDateFormat("YYYYMMdd-HHmmss");
1012
private static final String PASSED = "PASSED";
1113
private static final String FAILED = "FAILED";
12-
private static final String FILENAME_FORMAT = "%s-%s-%s.flv";
14+
private static final String FILENAME_FORMAT = "%s-%s-%s.%s";
15+
1316

1417
@Override
1518
public File recordingFileForTest(File vncRecordingDirectory, String prefix, boolean succeeded) {
19+
return recordingFileForTest(vncRecordingDirectory, prefix, succeeded, VncRecordingContainer.DEFAULT_RECORDING_FORMAT);
20+
}
21+
22+
@Override
23+
public File recordingFileForTest(File vncRecordingDirectory, String prefix, boolean succeeded, VncRecordingFormat recordingFormat) {
1624
final String resultMarker = succeeded ? PASSED : FAILED;
1725
final String fileName = String.format(FILENAME_FORMAT,
1826
resultMarker,
1927
prefix,
20-
filenameDateFormat.format(new Date())
28+
filenameDateFormat.format(new Date()),
29+
recordingFormat.getFilenameExtension()
2130
);
2231
return new File(vncRecordingDirectory, fileName);
2332
}

modules/selenium/src/main/java/org/testcontainers/containers/RecordingFileFactory.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package org.testcontainers.containers;
22

33
import org.junit.runner.Description;
4+
import org.testcontainers.containers.VncRecordingContainer.VncRecordingFormat;
45

56
import java.io.File;
67

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

15+
default File recordingFileForTest(File vncRecordingDirectory, String prefix, boolean succeeded, VncRecordingFormat recordingFormat) {
16+
return recordingFileForTest(vncRecordingDirectory, prefix, succeeded);
17+
}
18+
1419
File recordingFileForTest(File vncRecordingDirectory, String prefix, boolean succeeded);
20+
1521
}

modules/selenium/src/test/java/org/testcontainers/containers/DefaultRecordingFileFactoryTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,4 +57,4 @@ public void recordingFileThatShouldDescribeTheTestResultAtThePresentTime() throw
5757

5858
assertThat(expectedPossibleFileNames, hasItem(recordingFile));
5959
}
60-
}
60+
}

0 commit comments

Comments
 (0)