diff --git a/core/src/main/java/org/testcontainers/containers/VncRecordingContainer.java b/core/src/main/java/org/testcontainers/containers/VncRecordingContainer.java index de720110a51..a171e6ba2f6 100644 --- a/core/src/main/java/org/testcontainers/containers/VncRecordingContainer.java +++ b/core/src/main/java/org/testcontainers/containers/VncRecordingContainer.java @@ -2,6 +2,7 @@ import lombok.Getter; import lombok.NonNull; +import lombok.RequiredArgsConstructor; import lombok.SneakyThrows; import lombok.ToString; import org.apache.commons.compress.archivers.tar.TarArchiveInputStream; @@ -9,6 +10,7 @@ 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; @@ -25,26 +27,30 @@ @ToString public class VncRecordingContainer extends GenericContainer { - 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")) ); } @@ -52,7 +58,7 @@ public VncRecordingContainer(@NonNull GenericContainer targetContainer) { * 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); @@ -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; @@ -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; + } + } diff --git a/docs/modules/webdriver_containers.md b/docs/modules/webdriver_containers.md index 40b27a5207f..41b7fb6bb23 100644 --- a/docs/modules/webdriver_containers.md +++ b/docs/modules/webdriver_containers.md @@ -63,7 +63,14 @@ just for failing tests. [Record failing Tests](../../modules/selenium/src/test/java/org/testcontainers/junit/ChromeRecordingWebDriverContainerTest.java) inside_block:recordFailing -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: + + +[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 + 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: diff --git a/modules/selenium/src/main/java/org/testcontainers/containers/BrowserWebDriverContainer.java b/modules/selenium/src/main/java/org/testcontainers/containers/BrowserWebDriverContainer.java index 1ceaf67ad2c..4a25ae486cd 100644 --- a/modules/selenium/src/main/java/org/testcontainers/containers/BrowserWebDriverContainer.java +++ b/modules/selenium/src/main/java/org/testcontainers/containers/BrowserWebDriverContainer.java @@ -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; @@ -66,6 +67,7 @@ public class BrowserWebDriverContainer chrome = new BrowserWebDriverContainer<>() .withCapabilities(new ChromeOptions()) .withRecordingMode(RECORD_ALL, target) @@ -40,25 +57,96 @@ public void recordingTestThatShouldBeRecordedAndRetained() { .withRecordingFileFactory(new DefaultRecordingFileFactory()) .withNetwork(NETWORK) ) { - chrome.start(); + File[] files = runSimpleExploreInContainer(chrome, "PASSED-.*\\.flv"); + assertEquals("Recorded file not found", 1, files.length); + } + } - doSimpleExplore(chrome); - chrome.afterTest(new TestDescription() { - @Override - public String getTestId() { - return getFilesystemFriendlyName(); - } + private File[] runSimpleExploreInContainer(BrowserWebDriverContainer container, String fileNamePattern) throws InterruptedException { + container.start(); - @Override - public String getFilesystemFriendlyName() { - return "ChromeThatRecordsAllTests-recordingTestThatShouldBeRecordedAndRetained"; - } - }, Optional.empty()); + TimeUnit.MILLISECONDS.sleep(MINIMUM_VIDEO_DURATION_MILLISECONDS); + doSimpleExplore(container); + container.afterTest(new TestDescription() { + @Override + public String getTestId() { + return getFilesystemFriendlyName(); + } + + @Override + public String getFilesystemFriendlyName() { + return "ChromeThatRecordsAllTests-recordingTestThatShouldBeRecordedAndRetained"; + } + }, Optional.empty()); + + return vncRecordingDirectory.getRoot().listFiles(new PatternFilenameFilter(fileNamePattern)); + } - String[] files = vncRecordingDirectory.getRoot().list(new PatternFilenameFilter("PASSED-.*\\.flv")); + @Test + public void recordingTestShouldHaveFlvExtension() throws InterruptedException { + File target = vncRecordingDirectory.getRoot(); + try ( + // recordFlv { + // Set (explicitly) FLV format for recorded video: + BrowserWebDriverContainer chrome = new BrowserWebDriverContainer<>() + .withCapabilities(new ChromeOptions()) + .withRecordingMode(RECORD_ALL, target, VncRecordingFormat.FLV) + // } + .withRecordingFileFactory(new DefaultRecordingFileFactory()) + .withNetwork(NETWORK) + ) { + File[] files = runSimpleExploreInContainer(chrome, "PASSED-.*\\.flv"); assertEquals("Recorded file not found", 1, files.length); } } + + @Test + public void recordingTestShouldHaveMp4Extension() throws InterruptedException { + File target = vncRecordingDirectory.getRoot(); + try ( + // recordMp4 { + // Set MP4 format for recorded video: + BrowserWebDriverContainer chrome = new BrowserWebDriverContainer<>() + .withCapabilities(new ChromeOptions()) + .withRecordingMode(RECORD_ALL, target, VncRecordingFormat.MP4) + // } + .withRecordingFileFactory(new DefaultRecordingFileFactory()) + .withNetwork(NETWORK) + ) { + File[] files = runSimpleExploreInContainer(chrome, "PASSED-.*\\.mp4"); + assertEquals("Recorded file not found", 1, files.length); + } + } + + @Test + public void recordingTestThatShouldHaveCorrectDuration() throws IOException, InterruptedException { + MountableFile mountableFile; + try ( + BrowserWebDriverContainer chrome = new BrowserWebDriverContainer<>() + .withCapabilities(new ChromeOptions()) + .withRecordingMode(RECORD_ALL, vncRecordingDirectory.getRoot()) + .withRecordingFileFactory(new DefaultRecordingFileFactory()) + .withNetwork(NETWORK) + ) { + File[] recordedFiles = runSimpleExploreInContainer(chrome, "PASSED-.*\\.flv"); + mountableFile = MountableFile.forHostPath(recordedFiles[0].getCanonicalPath()); + } + + try (GenericContainer container = new GenericContainer<>(DockerImageName.parse("testcontainers/vnc-recorder:1.2.0"))) { + String recordFileContainerPath = "/tmp/chromeTestRecord.flv"; + container.withCopyFileToContainer(mountableFile, recordFileContainerPath) + .withCreateContainerCmdModifier(createContainerCmd -> createContainerCmd.withEntrypoint("ffmpeg")) + .withCommand("-i", recordFileContainerPath, "-f", "null", "-") + .waitingFor(new LogMessageWaitStrategy() + .withRegEx(".*Duration.*") + .withStartupTimeout(Duration.of(60, SECONDS))) + .start(); + String ffmpegOutput = container.getLogs(); + + assertTrue("Duration is incorrect in:\n " + ffmpegOutput, + ffmpegOutput.contains("Duration: 00:") && !(ffmpegOutput.contains("Duration: 00:00:00.00"))); + } + } } public static class ChromeThatRecordsFailingTests { @@ -85,7 +173,7 @@ public void recordingTestThatShouldBeRecordedButNotPersisted() { } @Test - public void recordingTestThatShouldBeRecordedAndRetained() { + public void recordingTestThatShouldBeRecordedAndRetained() throws InterruptedException { File target = vncRecordingDirectory.getRoot(); try ( // recordFailing { @@ -99,6 +187,7 @@ public void recordingTestThatShouldBeRecordedAndRetained() { ) { chrome.start(); + TimeUnit.MILLISECONDS.sleep(MINIMUM_VIDEO_DURATION_MILLISECONDS); doSimpleExplore(chrome); chrome.afterTest(new TestDescription() { @Override