我创建了一个屏幕录像机,而不是通过拍摄大量屏幕截图来录制屏幕,然后将所有屏幕截图转换为mov文件。
然而,mov文件的播放速度比正常情况快2倍,我在多台计算机上尝试过,结果仍然相同。
这是我的代码
主要课程
package recorder;
import javafx.application.Application;
import static javafx.application.Application.launch;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.stage.Stage;
public class Recorder extends Application {
public Stage stage;
@Override
public void start(Stage stage) throws Exception {
Parent root = FXMLLoader.load(getClass().getResource("FXMLDocument.fxml"));
Scene scene = new Scene(root);
stage.setResizable(false);
stage.setScene(scene);
stage.show();
}
public static void main(String[] args) {
launch(args);
}
}
FXMLDocument
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.TextArea?>
<?import javafx.scene.control.TextField?>
<?import javafx.scene.layout.ColumnConstraints?>
<?import javafx.scene.layout.GridPane?>
<?import javafx.scene.layout.RowConstraints?>
<GridPane maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="639.0" prefWidth="578.0" xmlns="http://javafx.com/javafx/8.0.60" xmlns:fx="http://javafx.com/fxml/1" fx:controller="recorder.FXMLDocumentController">
<columnConstraints>
<ColumnConstraints hgrow="SOMETIMES" minWidth="578.0" prefWidth="578.0" />
</columnConstraints>
<rowConstraints>
<RowConstraints maxHeight="1.7976931348623157E308" minHeight="10.0" prefHeight="95.0" vgrow="SOMETIMES" />
<RowConstraints maxHeight="245.0" minHeight="9.0" prefHeight="12.0" vgrow="SOMETIMES" />
<RowConstraints maxHeight="481.0" minHeight="10.0" prefHeight="478.0" vgrow="SOMETIMES" />
</rowConstraints>
<children>
<Button fx:id="start" maxHeight="1.7976931348623157E308" maxWidth="1.7976931348623157E308" mnemonicParsing="false" onAction="#startOnAction" prefHeight="220.0" prefWidth="578.0" text="Start Recording" />
<TextArea fx:id="console" editable="false" prefHeight="200.0" prefWidth="200.0" promptText="Console" wrapText="true" GridPane.rowIndex="2" />
<TextField fx:id="input" prefHeight="105.0" prefWidth="578.0" promptText="File Name" GridPane.rowIndex="1" />
</children>
</GridPane>
FXMLDocumentController
package recorder;
import java.awt.AWTException;
import java.awt.Dimension;
import java.awt.Rectangle;
import java.awt.Robot;
import java.awt.Toolkit;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ResourceBundle;
import java.util.Timer;
import java.util.TimerTask;
import java.util.Vector;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import java.util.logging.Logger;
import javafx.beans.value.ObservableValue;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.control.Button;
import javafx.scene.control.TextArea;
import javafx.scene.control.TextField;
import javafx.stage.Stage;
import javax.imageio.ImageIO;
import javax.media.MediaLocator;
import javax.swing.JOptionPane;
public class FXMLDocumentController implements Initializable {
ExecutorService imageSavingService = new ThreadPoolExecutor(2, 2,
0L, TimeUnit.MILLISECONDS,
new ArrayBlockingQueue<Runnable>(20), new ThreadPoolExecutor.DiscardPolicy());
boolean recording;
File file;
int num;
/**
* Screen Width.
*/
public static int screenWidth = (int) Toolkit.getDefaultToolkit()
.getScreenSize().getWidth();
/**
* Screen Height.
*/
public static int screenHeight = (int) Toolkit.getDefaultToolkit()
.getScreenSize().getHeight();
/**
* Interval between which the image needs to be captured.
*/
public static int captureInterval = 25;
/**
* Temporary folder to store the screenshot.
*/
public static String store = "tmp";
/**
* Status of the recorder.
*/
public static boolean record = false;
@FXML
private Button start;
@FXML
private TextArea console;
@FXML
private TextField input;
public static void startRecord() {
FXMLDocumentController t = new FXMLDocumentController();
// Thread recordThread = new Thread() {
// @Override
// public void run() {
// Robot rt;
// int cnt = 0;
// try {
// rt = new Robot();
// while (cnt == 0 || record) {
// BufferedImage img = rt.createScreenCapture(new Rectangle(screenWidth, screenHeight));
// ImageIO.write(img, "jpeg", new File("./" + store + "/"
// + System.currentTimeMillis() + ".jpeg"));
// if (cnt == 0) {
// record = true;
// cnt = 1;
// }
// t.wait(captureInterval);
// }
// } catch (Exception e) {
// e.printStackTrace();
// }
// }
// };
// recordThread.start();
Timer timer = new Timer();
timer.scheduleAtFixedRate(new TimerTask() {
@Override
public void run() {
try {
Robot rt;
int cnt = 0;
rt = new Robot();
BufferedImage img = rt.createScreenCapture(new Rectangle(screenWidth, screenHeight));
final long timeStemp = System.currentTimeMillis();
t.imageSavingService.submit(() -> {
try {
ImageIO.write(img, "jpeg", new File("./" + store + "/"
+ timeStemp + ".jpeg"));
} catch (IOException e) {
//handle exception (e.g. via callback)
}
});
if (cnt == 0) {
record = true;
cnt = 1;
}
} catch (AWTException ex) {
}
}
}, captureInterval, captureInterval);
}
public void makeVideo(String movFile) throws MalformedURLException {
imageSavingService.shutdown();
try {
while (!imageSavingService.awaitTermination(5, TimeUnit.SECONDS)) {
// waiting another 5 seconds for the service to terminate
}
} catch (InterruptedException e1) {
e1.printStackTrace();
}
this.console.appendText("Processing Video... please wait");
JpegImagesToMovie imageToMovie = new JpegImagesToMovie();
Vector<String> imgLst = new Vector<String>();
File f = new File(store);
File[] fileLst = f.listFiles();
for (int i = 0; i < fileLst.length; i++) {
imgLst.add(fileLst[i].getAbsolutePath());
}
// Generate the output media locators.
MediaLocator oml;
if ((oml = imageToMovie.createMediaLocator(movFile)) == null) {
this.console.appendText("Error in processing");
System.exit(0);
}
imageToMovie.doIt(screenWidth, screenHeight, (1000 / captureInterval), imgLst, oml);
f.deleteOnExit();
System.exit(0);
}
@FXML
void startOnAction(ActionEvent event) {
recording = true;
Timer timer = new Timer();
this.console.setText("######### Starting Screen Recorder #########\n");
Dimension screen = Toolkit.getDefaultToolkit().getScreenSize();
this.console.appendText("Your Screen [Width, Height]: " + "[" + screen.getWidth() + "," + screen.getHeight() + "]\nWhen the Recording starts, this window will minimize, to stop them recording, open this window.");
try {
Thread.sleep(500);
} catch (InterruptedException ex) {
}
this.console.appendText("The recording will start NOW\n");
num = 5;
File f = new File(store);
if (!f.exists()) {
f.mkdir();
}
startRecord();
Stage stage = (Stage) console.getScene().getWindow();
stage.setIconified(true);
stage.focusedProperty().addListener((ObservableValue<? extends Boolean> observable, Boolean oldValue, Boolean newValue) -> {
focusState(newValue);
});
}
private void focusState(boolean value) {
if (recording) {
try {
makeVideo(input.getText() + ".mov");
recording = false;
// if (value) {
// record = false;
// try {
// makeVideo(input.getText() + ".mov");
// } catch (MalformedURLException ex) {
// }
// } else {
// }
} catch (MalformedURLException ex) {
}
}
}
@Override
public void initialize(URL url, ResourceBundle rb) {
}
}
JpegImagesToMovie(不是我创造的)
package recorder;
/*
* @(#)JpegImagesToMovie.java 1.3 01/03/13
*
* Copyright (c) 1999-2001 Sun Microsystems, Inc. All Rights Reserved.
*
* Sun grants you ("Licensee") a non-exclusive, royalty free, license to use,
* modify and redistribute this software in source and binary code form,
* provided that i) this copyright notice and license appear on all copies of
* the software; and ii) Licensee does not utilize the software in a manner
* which is disparaging to Sun.
*
* This software is provided "AS IS," without a warranty of any kind. ALL
* EXPRESS OR IMPLIED CONDITIONS, REPRESENTATIONS AND WARRANTIES, INCLUDING ANY
* IMPLIED WARRANTY OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE OR
* NON-INFRINGEMENT, ARE HEREBY EXCLUDED. SUN AND ITS LICENSORS SHALL NOT BE
* LIABLE FOR ANY DAMAGES SUFFERED BY LICENSEE AS A RESULT OF USING, MODIFYING
* OR DISTRIBUTING THE SOFTWARE OR ITS DERIVATIVES. IN NO EVENT WILL SUN OR ITS
* LICENSORS BE LIABLE FOR ANY LOST REVENUE, PROFIT OR DATA, OR FOR DIRECT,
* INDIRECT, SPECIAL, CONSEQUENTIAL, INCIDENTAL OR PUNITIVE DAMAGES, HOWEVER
* CAUSED AND REGARDLESS OF THE THEORY OF LIABILITY, ARISING OUT OF THE USE OF
* OR INABILITY TO USE SOFTWARE, EVEN IF SUN HAS BEEN ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGES.
*
* This software is not designed or intended for use in on-line control of
* aircraft, air traffic, aircraft navigation or aircraft communications; or in
* the design, construction, operation or maintenance of any nuclear
* facility. Licensee represents and warrants that it will not use or
* redistribute the Software for such purposes.
*/
import java.awt.Dimension;
import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.net.MalformedURLException;
import java.util.Vector;
import javax.media.Buffer;
import javax.media.ConfigureCompleteEvent;
import javax.media.ControllerEvent;
import javax.media.ControllerListener;
import javax.media.DataSink;
import javax.media.EndOfMediaEvent;
import javax.media.Format;
import javax.media.Manager;
import javax.media.MediaLocator;
import javax.media.PrefetchCompleteEvent;
import javax.media.Processor;
import javax.media.RealizeCompleteEvent;
import javax.media.ResourceUnavailableEvent;
import javax.media.Time;
import javax.media.control.TrackControl;
import javax.media.datasink.DataSinkErrorEvent;
import javax.media.datasink.DataSinkEvent;
import javax.media.datasink.DataSinkListener;
import javax.media.datasink.EndOfStreamEvent;
import javax.media.format.VideoFormat;
import javax.media.protocol.ContentDescriptor;
import javax.media.protocol.DataSource;
import javax.media.protocol.FileTypeDescriptor;
import javax.media.protocol.PullBufferDataSource;
import javax.media.protocol.PullBufferStream;
/**
* This program takes a list of JPEG image files and convert them into a
* QuickTime movie.
*/
public class JpegImagesToMovie implements ControllerListener, DataSinkListener {
public boolean doIt(int width, int height, int frameRate, Vector inFiles,
MediaLocator outML) throws MalformedURLException {
ImageDataSource ids = new ImageDataSource(width, height, frameRate,
inFiles);
Processor p;
try {
//System.err
// .println("- create processor for the image datasource ...");
p = Manager.createProcessor(ids);
} catch (Exception e) {
System.err
.println("Yikes! Cannot create a processor from the data source.");
return false;
}
p.addControllerListener(this);
// Put the Processor into configured state so we can set
// some processing options on the processor.
p.configure();
if (!waitForState(p, p.Configured)) {
System.err.println("Failed to configure the processor.");
return false;
}
// Set the output content descriptor to QuickTime.
p.setContentDescriptor(new ContentDescriptor(
FileTypeDescriptor.QUICKTIME));
// Query for the processor for supported formats.
// Then set it on the processor.
TrackControl tcs[] = p.getTrackControls();
Format f[] = tcs[0].getSupportedFormats();
if (f == null || f.length <= 0) {
System.err.println("The mux does not support the input format: "
+ tcs[0].getFormat());
return false;
}
tcs[0].setFormat(f[0]);
//System.err.println("Setting the track format to: " + f[0]);
// We are done with programming the processor. Let's just
// realize it.
p.realize();
if (!waitForState(p, p.Realized)) {
System.err.println("Failed to realize the processor.");
return false;
}
// Now, we'll need to create a DataSink.
DataSink dsink;
if ((dsink = createDataSink(p, outML)) == null) {
System.err
.println("Failed to create a DataSink for the given output MediaLocator: "
+ outML);
return false;
}
dsink.addDataSinkListener(this);
fileDone = false;
System.out.println("Generating the video : "+outML.getURL().toString());
// OK, we can now start the actual transcoding.
try {
p.start();
dsink.start();
} catch (IOException e) {
System.err.println("IO error during processing");
return false;
}
// Wait for EndOfStream event.
waitForFileDone();
// Cleanup.
try {
dsink.close();
} catch (Exception e) {
}
p.removeControllerListener(this);
System.out.println("Video creation completed!!!!!");
return true;
}
/**
* Create the DataSink.
*/
DataSink createDataSink(Processor p, MediaLocator outML) {
DataSource ds;
if ((ds = p.getDataOutput()) == null) {
System.err
.println("Something is really wrong: the processor does not have an output DataSource");
return null;
}
DataSink dsink;
try {
//System.err.println("- create DataSink for: " + outML);
dsink = Manager.createDataSink(ds, outML);
dsink.open();
} catch (Exception e) {
System.err.println("Cannot create the DataSink: " + e);
return null;
}
return dsink;
}
Object waitSync = new Object();
boolean stateTransitionOK = true;
/**
* Block until the processor has transitioned to the given state. Return
* false if the transition failed.
*/
boolean waitForState(Processor p, int state) {
synchronized (waitSync) {
try {
while (p.getState() < state && stateTransitionOK)
waitSync.wait();
} catch (Exception e) {
}
}
return stateTransitionOK;
}
/**
* Controller Listener.
*/
public void controllerUpdate(ControllerEvent evt) {
if (evt instanceof ConfigureCompleteEvent
|| evt instanceof RealizeCompleteEvent
|| evt instanceof PrefetchCompleteEvent) {
synchronized (waitSync) {
stateTransitionOK = true;
waitSync.notifyAll();
}
} else if (evt instanceof ResourceUnavailableEvent) {
synchronized (waitSync) {
stateTransitionOK = false;
waitSync.notifyAll();
}
} else if (evt instanceof EndOfMediaEvent) {
evt.getSourceController().stop();
evt.getSourceController().close();
}
}
Object waitFileSync = new Object();
boolean fileDone = false;
boolean fileSuccess = true;
/**
* Block until file writing is done.
*/
boolean waitForFileDone() {
synchronized (waitFileSync) {
try {
while (!fileDone)
waitFileSync.wait();
} catch (Exception e) {
}
}
return fileSuccess;
}
/**
* Event handler for the file writer.
*/
public void dataSinkUpdate(DataSinkEvent evt) {
if (evt instanceof EndOfStreamEvent) {
synchronized (waitFileSync) {
fileDone = true;
waitFileSync.notifyAll();
}
} else if (evt instanceof DataSinkErrorEvent) {
synchronized (waitFileSync) {
fileDone = true;
fileSuccess = false;
waitFileSync.notifyAll();
}
}
}
/*public static void main(String args[]) {
if (args.length == 0)
prUsage();
// Parse the arguments.
int i = 0;
int width = -1, height = -1, frameRate = 1;
Vector inputFiles = new Vector();
String outputURL = null;
while (i < args.length) {
if (args[i].equals("-w")) {
i++;
if (i >= args.length)
prUsage();
width = new Integer(args[i]).intValue();
} else if (args[i].equals("-h")) {
i++;
if (i >= args.length)
prUsage();
height = new Integer(args[i]).intValue();
} else if (args[i].equals("-f")) {
i++;
if (i >= args.length)
prUsage();
frameRate = new Integer(args[i]).intValue();
} else if (args[i].equals("-o")) {
i++;
if (i >= args.length)
prUsage();
outputURL = args[i];
} else {
inputFiles.addElement(args[i]);
}
i++;
}
if (outputURL == null || inputFiles.size() == 0)
prUsage();
// Check for output file extension.
if (!outputURL.endsWith(".mov") && !outputURL.endsWith(".MOV")) {
System.err
.println("The output file extension should end with a .mov extension");
prUsage();
}
if (width < 0 || height < 0) {
System.err.println("Please specify the correct image size.");
prUsage();
}
// Check the frame rate.
if (frameRate < 1)
frameRate = 1;
// Generate the output media locators.
MediaLocator oml;
if ((oml = createMediaLocator(outputURL)) == null) {
System.err.println("Cannot build media locator from: " + outputURL);
System.exit(0);
}
JpegImagesToMovie imageToMovie = new JpegImagesToMovie();
imageToMovie.doIt(width, height, frameRate, inputFiles, oml);
System.exit(0);
}*/
static void prUsage() {
System.err
.println("Usage: java JpegImagesToMovie -w <width> -h <height> -f <frame rate> -o <output URL> <input JPEG file 1> <input JPEG file 2> ...");
System.exit(-1);
}
/**
* Create a media locator from the given string.
*/
static MediaLocator createMediaLocator(String url) {
MediaLocator ml;
if (url.indexOf(":") > 0 && (ml = new MediaLocator(url)) != null)
return ml;
if (url.startsWith(File.separator)) {
if ((ml = new MediaLocator("file:" + url)) != null)
return ml;
} else {
String file = "file:" + System.getProperty("user.dir")
+ File.separator + url;
if ((ml = new MediaLocator(file)) != null)
return ml;
}
return null;
}
// /////////////////////////////////////////////
//
// Inner classes.
// /////////////////////////////////////////////
/**
* A DataSource to read from a list of JPEG image files and turn that into a
* stream of JMF buffers. The DataSource is not seekable or positionable.
*/
class ImageDataSource extends PullBufferDataSource {
ImageSourceStream streams[];
ImageDataSource(int width, int height, int frameRate, Vector images) {
streams = new ImageSourceStream[1];
streams[0] = new ImageSourceStream(width, height, frameRate, images);
}
public void setLocator(MediaLocator source) {
}
public MediaLocator getLocator() {
return null;
}
/**
* Content type is of RAW since we are sending buffers of video frames
* without a container format.
*/
public String getContentType() {
return ContentDescriptor.RAW;
}
public void connect() {
}
public void disconnect() {
}
public void start() {
}
public void stop() {
}
/**
* Return the ImageSourceStreams.
*/
public PullBufferStream[] getStreams() {
return streams;
}
/**
* We could have derived the duration from the number of frames and
* frame rate. But for the purpose of this program, it's not necessary.
*/
public Time getDuration() {
return DURATION_UNKNOWN;
}
public Object[] getControls() {
return new Object[0];
}
public Object getControl(String type) {
return null;
}
}
/**
* The source stream to go along with ImageDataSource.
*/
class ImageSourceStream implements PullBufferStream {
Vector images;
int width, height;
VideoFormat format;
int nextImage = 0; // index of the next image to be read.
boolean ended = false;
public ImageSourceStream(int width, int height, int frameRate,
Vector images) {
this.width = width;
this.height = height;
this.images = images;
format = new VideoFormat(VideoFormat.JPEG, new Dimension(width,
height), Format.NOT_SPECIFIED, Format.byteArray,
(float) frameRate);
}
/**
* We should never need to block assuming data are read from files.
*/
public boolean willReadBlock() {
return false;
}
/**
* This is called from the Processor to read a frame worth of video
* data.
*/
public void read(Buffer buf) throws IOException {
// Check if we've finished all the frames.
if (nextImage >= images.size()) {
// We are done. Set EndOfMedia.
//System.err.println("Done reading all images.");
buf.setEOM(true);
buf.setOffset(0);
buf.setLength(0);
ended = true;
return;
}
String imageFile = (String) images.elementAt(nextImage);
nextImage++;
//System.err.println(" - reading image file: " + imageFile);
// Open a random access file for the next image.
RandomAccessFile raFile;
raFile = new RandomAccessFile(imageFile, "r");
byte data[] = null;
// Check the input buffer type & size.
if (buf.getData() instanceof byte[])
data = (byte[]) buf.getData();
// Check to see the given buffer is big enough for the frame.
if (data == null || data.length < raFile.length()) {
data = new byte[(int) raFile.length()];
buf.setData(data);
}
// Read the entire JPEG image from the file.
raFile.readFully(data, 0, (int) raFile.length());
//System.err.println(" read " + raFile.length() + " bytes.");
buf.setOffset(0);
buf.setLength((int) raFile.length());
buf.setFormat(format);
buf.setFlags(buf.getFlags() | buf.FLAG_KEY_FRAME);
// Close the random access file.
raFile.close();
}
/**
* Return the format of each video frame. That will be JPEG.
*/
public Format getFormat() {
return format;
}
public ContentDescriptor getContentDescriptor() {
return new ContentDescriptor(ContentDescriptor.RAW);
}
public long getContentLength() {
return 0;
}
public boolean endOfStream() {
return ended;
}
public Object[] getControls() {
return new Object[0];
}
public Object getControl(String type) {
return null;
}
}
}
答案 0 :(得分:2)
问题是将图像写入磁盘非常慢。这意味着在您的情况下,截取屏幕+将其写入磁盘大约需要50毫秒 - &gt;只有大约每100毫秒(50毫秒捕获+保存,50毫秒睡眠)保存屏幕截图 - &gt;结果视频的速度是原来的两倍。
可能的解决方案:
首先,您应该使用java.util.Timer
来安排屏幕截图,而不是仅考虑Thread.sleep()
,而不考虑任务所需的时间。使用计时器,您可以使用50毫秒的周期性固定延迟来安排任务。
下一步是找到一种如何更快地保存捕获图像的方法。
一种方法是将图像临时保存到线程安全队列,并将&#34;映像外包到磁盘&#34; -process到一个单独的线程,该线程与屏幕捕获过程并行运行。理想情况下,这个单独的线程应该将图像直接编码为视频格式,但这需要您提出自己的JpegImagesToMovie
版本(这意味着直接处理Java Media Framework)。
加快图像保存过程可以这样做:
创建ExecutorService
以便在录制开始时保存屏幕截图:
imageSavingService = new ThreadPoolExecutor(2, 2,
0L, TimeUnit.MILLISECONDS,
new ArrayBlockingQueue<Runnable>(20), new ThreadPoolExecutor.DiscardPolicy());
这将创建一个池大小为2且工作队列容量为20的执行程序。将工作队列限制为指定容量将阻止超出内存。如果队列已满,则将丢弃新提交的帧(不应该发生)。通常单个线程应该是IO操作的最佳选择,但是当我测试它时,两个线程的表现要好得多。
您可以向服务提交图像保存任务,如下所示:
final long timeStemp = System.currentTimeMillis();
imageSavingService.submit(() -> {
try {
ImageIO.write(img, "jpeg", new File("./" + store + "/"
+ timeStemp + ".jpeg"));
} catch (IOException e) {
//handle exception (e.g. via callback)
}
});
取消录制后,您可以关闭此服务:
imageSavingService.shutdown();
try {
while(!imageSavingService.awaitTermination(5, TimeUnit.SECONDS)){
// waiting another 5 seconds for the service to terminate
}
} catch (InterruptedException e1) {
e1.printStackTrace();
}
这样您可以按顺序关闭服务,并等待处理上一个待处理的图像保存任务。
使用此服务加速图像保存以及建议的计时器使我能够以50毫秒的捕获间隔进行记录而不会丢失帧。