Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Bug: Framerate of videorecoding is not intuitive #303 #309

Merged
Merged
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 @@ -2,6 +2,7 @@

import static org.junit.Assert.assertTrue;

import java.io.FileNotFoundException;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
Expand Down Expand Up @@ -138,6 +139,10 @@ private static TakeScreenshotsThread createTakeScreenshotsThread(String testName
catch (IOException | InstantiationException | IllegalAccessException | IllegalArgumentException | InvocationTargetException | NoSuchMethodException
| SecurityException e)
{
if (e instanceof FileNotFoundException)
{
throw new RuntimeException(e.getMessage(), e);
}
throw new RuntimeException("thread couldn't be created", e);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,17 +78,19 @@ public synchronized void run()

long turns = 0;
long millis = 0;

long duration = 200;
// start screenshot loop
while (run)
{
long start = new Date().getTime();

File file = ((TakesScreenshot) driver).getScreenshotAs(OutputType.FILE);
writer.compressImageIfNeeded(file, recordingConfigurations.imageScaleFactor(), recordingConfigurations.imageQuality());
writer.write(file);

long duration = new Date().getTime() - start;
long delay = recordingConfigurations.oneImagePerMilliseconds() > duration ? recordingConfigurations.oneImagePerMilliseconds()
: duration;
writer.write(file, delay);
file.delete();
duration = new Date().getTime() - start;
millis += duration;
turns++;
long sleep = recordingConfigurations.oneImagePerMilliseconds() - duration;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,13 +59,13 @@ protected GifSequenceWriter(RecordingConfigurations recordingConfigurations, Str
/**
* Configures metadata, which will be used for every gif sequence
*
* @param delay
* @param duration
* {@link Integer} value of milliseconds between the neighbor gif sequences
* @param loop
* {@link Boolean} value if the gif should be looped
* @throws IIOInvalidTreeException
*/
private void configureRootMetadata(int delay, boolean loop) throws IIOInvalidTreeException
private void configureRootMetadata(long duration, boolean loop) throws IIOInvalidTreeException
{
String metaFormatName = metadata.getNativeMetadataFormatName();
IIOMetadataNode root = (IIOMetadataNode) metadata.getAsTree(metaFormatName);
Expand All @@ -74,7 +74,7 @@ private void configureRootMetadata(int delay, boolean loop) throws IIOInvalidTre
graphicsControlExtensionNode.setAttribute("disposalMethod", "none");
graphicsControlExtensionNode.setAttribute("userInputFlag", "FALSE");
graphicsControlExtensionNode.setAttribute("transparentColorFlag", "FALSE");
graphicsControlExtensionNode.setAttribute("delayTime", Integer.toString(delay / 10));
graphicsControlExtensionNode.setAttribute("delayTime", duration / 10 + "");
graphicsControlExtensionNode.setAttribute("transparentColorIndex", "0");

IIOMetadataNode commentsNode = getNode(root, "CommentExtensions");
Expand Down Expand Up @@ -145,11 +145,12 @@ public void start() throws IOException
* Writes a gif sequence into gif file
*/
@Override
public void write(File image)
public void write(File image, long duration)
{
try
{
BufferedImage img = ImageIO.read(image);
configureRootMetadata(duration, false);
writer.writeToSequence(new IIOImage(img, null, metadata), params);
}
catch (IOException e)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,17 @@
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.OutputStream;
import java.lang.ProcessBuilder.Redirect;
import java.util.Date;
import java.util.UUID;

import javax.imageio.ImageIO;
import javax.imageio.stream.ImageInputStream;

import org.apache.commons.lang3.math.Fraction;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

Expand All @@ -30,14 +33,20 @@ public class VideoWriter implements Writer
{
private static final Logger LOGGER = LoggerFactory.getLogger(VideoWriter.class);

private VideoRecordingConfigurations recordingConfigurations;

private int screenshots;

private long testDuration;

private String videoFileName;

private ProcessBuilder pb;

private OutputStream ffmpegInput;

private Process p;

private boolean logVideoProcessingTime;

/**
* Required to instantiate the object in the {@link Writer#instantiate(Class, RecordingConfigurations, String)}
* method.
Expand All @@ -48,17 +57,30 @@ public class VideoWriter implements Writer
* {@link VideoRecordingConfigurations} for the writer
* @param videoFileName
* {@link String} video file name ( including the path)
* @throws IOException
* @throws FileNotFoundException
*/
protected VideoWriter(RecordingConfigurations recordingConfigurations, String videoFileName) throws IOException
protected VideoWriter(RecordingConfigurations recordingConfigurations, String videoFileName) throws FileNotFoundException
{
// check if ffmpeg binary is found
p = new ProcessBuilder(((VideoRecordingConfigurations) recordingConfigurations).ffmpegBinaryPath(), "-h").start();
this.recordingConfigurations = ((VideoRecordingConfigurations) recordingConfigurations);
String ffmpegBinary = this.recordingConfigurations.ffmpegBinaryPath();
try
{
p = new ProcessBuilder(ffmpegBinary, "-h").start();
}
catch (Exception e)
{
throw (FileNotFoundException) new FileNotFoundException("FFmpeg binary not found at " + ffmpegBinary
+ ", please install FFmpeg and add it to the PATH or enter the correct FFmpeg binary location. ").initCause(e);
}

pb = new ProcessBuilder(((VideoRecordingConfigurations) recordingConfigurations).ffmpegBinaryPath(), "-y", "-f", "image2pipe", "-i", "pipe:0", "-c:v", "libx264", "-strict", "-2", "-preset", "slow", "-pix_fmt", "yuv420p", "-vf", "scale=trunc(iw/2)*2:trunc(ih/2)*2", "-f", "mp4", videoFileName);
double framerate = 1 / ((double) recordingConfigurations.oneImagePerMilliseconds() / 1000);

this.videoFileName = videoFileName;
pb = new ProcessBuilder(((VideoRecordingConfigurations) recordingConfigurations).ffmpegBinaryPath(), "-y", "-f", "image2pipe", "-r", Fraction.getFraction(framerate)
.toString(), "-i", "pipe:0", "-c:v", "libx264", "-strict", "-2", "-preset", "slow", "-pix_fmt", "yuv420p", "-vf", "scale=trunc(iw/2)*2:trunc(ih/2)*2", "-f", "mp4", videoFileName);
pb.redirectErrorStream(true);
pb.redirectOutput(Redirect.appendTo(new File(((VideoRecordingConfigurations) recordingConfigurations).ffmpegLogFile())));
logVideoProcessingTime = recordingConfigurations.logInformationAboutRecording();
pb.redirectOutput(Redirect.appendTo(new File((this.recordingConfigurations).ffmpegLogFile())));
}

/**
Expand All @@ -78,7 +100,7 @@ public void start() throws IOException
* Writes {@link File} into the FFmpeg {@link Process}
*/
@Override
public void write(File image)
public void write(File image, long duration)
{
byte[] imageBytes;
imageBytes = new byte[(int) image.length()];
Expand All @@ -88,8 +110,9 @@ public void write(File image)

ImageInputStream iis = ImageIO.createImageInputStream(new ByteArrayInputStream(imageBytes));
BufferedImage img = ImageIO.read(iis);

ImageIO.write(img, "PNG", ffmpegInput);
screenshots++;
testDuration += duration;
}
catch (IOException e)
{
Expand Down Expand Up @@ -125,8 +148,33 @@ public void stop()
LOGGER.info("process video is processing");
Selenide.sleep(200);
}
File tempFile = new File(recordingConfigurations.tempFolderToStoreRecording() + "/" + "temp" + UUID.randomUUID() + ".mp4");
new File(videoFileName).renameTo(tempFile);
double actualFramerate = screenshots / ((double) testDuration / 1000);
pb = new ProcessBuilder(recordingConfigurations.ffmpegBinaryPath(), "-y", "-r", actualFramerate + "", "-i", tempFile.getPath(), videoFileName);

if (logVideoProcessingTime)
pb.redirectErrorStream(true);
pb.redirectOutput(Redirect.appendTo(new File((this.recordingConfigurations).ffmpegLogFile())));
try
{
p = pb.start();
}
catch (IOException e)
{
throw new RuntimeException("Could not adjust video frame rate", e);
}
while (p.isAlive())
{
if (new Date().getTime() - videoProcessingStart > 200000)
{
LOGGER.error("something went wrong with adjusting frame rate");
break;
}
LOGGER.info("video frame rate adjustment is processing");
Selenide.sleep(200);
}
tempFile.delete();
if (recordingConfigurations.logInformationAboutRecording())
{
AllureAddons.addToReport("video processing took " + (new Date().getTime() - videoProcessingStart + " ms"), "");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
Expand Down Expand Up @@ -48,8 +49,18 @@ public static Writer instantiate(Class<? extends Writer> writer, RecordingConfig
Writer writerObject = null;

Constructor<? extends Writer> constructor = writer.getDeclaredConstructor(RecordingConfigurations.class, String.class);
writerObject = constructor.newInstance(recordingConfigurations, fileName);

try
{
writerObject = constructor.newInstance(recordingConfigurations, fileName);
}
catch (InvocationTargetException e)
{
if (e.getCause() instanceof FileNotFoundException)
{
throw (FileNotFoundException) e.getCause();
}
throw e;
}
return writerObject;
}

Expand Down Expand Up @@ -149,7 +160,7 @@ public default BufferedImage resizeImage(BufferedImage originalImage, double sca

public void start() throws IOException;

public void write(File image);
public void write(File image, long delay);

public void stop();
}
29 changes: 13 additions & 16 deletions src/main/java/com/xceptance/neodymium/util/AllureAddons.java
Original file line number Diff line number Diff line change
Expand Up @@ -438,21 +438,17 @@ public static File getAllureResultsFolder()
public static void addLinkToReport(String message, String url)
{
}


public static void initializeEnvironmentInformation()
{
Map<String, String> environmentDataMap = new HashMap<String, String>();

if (!neoVersionLogged && Neodymium.configuration().logNeoVersion())
{
if (!AllureAddons.envFileExists())
{
LOGGER.info("This test uses Neodymium Library (version: " + Neodymium.getNeodymiumVersion()
+ "), MIT License, more details on https://github.com/Xceptance/neodymium-library");
neoVersionLogged = true;
environmentDataMap.putIfAbsent("Testing Framework", "Neodymium " + Neodymium.getNeodymiumVersion());
}
LOGGER.info("This test uses Neodymium Library (version: " + Neodymium.getNeodymiumVersion()
+ "), MIT License, more details on https://github.com/Xceptance/neodymium-library");
neoVersionLogged = true;
environmentDataMap.putIfAbsent("Testing Framework", "Neodymium " + Neodymium.getNeodymiumVersion());
}
if (!customDataAdded && Neodymium.configuration().enableCustomEnvironmentData())
{
Expand Down Expand Up @@ -489,27 +485,28 @@ public static void initializeEnvironmentInformation()
}
}


/**
*
* @param name
* of the attachment
* @param data
* that needs to be added as an attachment
*/
public static void addDataAsJsonToReport(String name, Object data)
public static void addDataAsJsonToReport(String name, Object data)
{
ObjectMapper mapper = new ObjectMapper();
String dataObjectJson;

try {

try
{
// covert Java object to JSON strings
dataObjectJson = mapper.setSerializationInclusion(Include.NON_NULL).writeValueAsString(data);

} catch (JsonProcessingException e) {

}
catch (JsonProcessingException e)
{
throw new RuntimeException(e);
}

Allure.addAttachment(name, "text/html", DataUtils.convertJsonToHtml(dataObjectJson), "html");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package com.xceptance.neodymium.junit4.testclasses.recording;

import java.io.IOException;
import java.util.UUID;

import org.junit.Test;
import org.junit.runner.RunWith;

import com.codeborne.selenide.Selenide;
import com.xceptance.neodymium.common.browser.Browser;
import com.xceptance.neodymium.common.recording.FilmTestExecution;
import com.xceptance.neodymium.junit4.NeodymiumRunner;

@Browser("Chrome_headless")
@RunWith(NeodymiumRunner.class)
public class AutomaticRecordingTest
{
public static String uuid;

public static boolean isGif;

@Test
public void test() throws IOException
{
uuid = UUID.randomUUID().toString();
if (isGif)
{
FilmTestExecution.startGifRecording(uuid);
}
else
{
FilmTestExecution.startVideoRecording(uuid);
}
Selenide.open("https://www.timeanddate.com/worldclock/germany/berlin");
Selenide.sleep(30000);

if (isGif)
{
FilmTestExecution.finishGifFilming(uuid, false);
}
else
{
FilmTestExecution.finishVideoFilming(uuid, false);
}
}
}
Loading