java实现屏幕录像的原理:对当前屏幕进行截图,然后将截图合成视频。根据配置(包括视频帧率:即截取屏幕的频率 截图质量 截图及视频文件存放地址)截图并生成视频文件。
- Config 一些配置文件
- ScreenRecording 截图工具类
- JpgToAviUtil 图片生成视频工具类
- ScreenRecordTest 测试类
Config配置文件
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.context.annotation.Configuration;
import org.springframework.stereotype.Component;
import java.awt.*;
public class Config {
// 截图及视频压缩比例 1.0为原画质
private float quality;
public static String SCREEN_FULL="FULL_SCREEN";
// 截图及视频文件存放地址
public String basePath;
// 截图编号
public int startNum = 1;
public int serialNum = 0;
public int getSerialNum() {
return serialNum;
}
public void setSerialNum(int serialNum) {
this.serialNum = serialNum;
}
public int getStartNum() {
return startNum;
}
public void setStartNum(int startNum) {
this.startNum = startNum;
}
public float getScale() {
return scale;
}
public void setScale(float scale) {
this.scale = scale;
}
// 按照比例进行缩放
public float scale;
// 视频帧率 即截取屏幕的频率
public int frame=60;
public Rectangle rectangle;
public String userName;
public float getQuality() {
return quality;
}
@Override
public String toString() {
return "Config{" +
"quality=" + quality +
", basePath='" + basePath + '\'' +
", frame=" + frame +
", rectangle=" + rectangle +
", userName='" + userName + '\'' +
'}';
}
public void setQuality(float quality) {
this.quality = quality;
}
public Rectangle getScreenFull() {
return new Rectangle(Toolkit.getDefaultToolkit().getScreenSize());//可以指定捕获屏幕区域
}
public String getBasePath() {
return basePath;
}
public void setBasePath(String basePath) {
this.basePath = basePath;
}
public String getUserName() {
return userName;
}
public void setUserName(String userName) {
this.userName = userName;
}
public int getFrame() {
return frame;
}
public void setFrame(int frame) {
this.frame = frame;
}
public Rectangle getRectangle() {
if (rectangle==null)
return getScreenFull();
return rectangle;
}
public void setRectangle(Rectangle rectangle) {
this.rectangle = rectangle;
}
}
ScreenRecording 录屏工具类
import net.coobird.thumbnailator.Thumbnails;
import org.apache.commons.lang3.tuple.ImmutablePair;
import org.jim2mov.core.MovieSaveException;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Date;
//录屏工具类
public class ScreenRecording {
Config config;
Robot robot;
static Long serialNum = Long.valueOf(0);
static Long shownum = Long.valueOf(1);
public Thread thread;
private long sleepTime;
private boolean recordSwitch = true;
private String recordName;
// 文件名
String time =new SimpleDateFormat("yyyyMMddHHmmss").format(new Date());
public ScreenRecording() throws AWTException {
robot = new Robot();
init();
}
*/
/**
* 录屏开始后返回该次录屏的线程ID以及录屏文件存放的位置
* @return
*//*
public ImmutablePair<Long,String> start() {
recordSwitch=true;
if (shownum < serialNum)
shownum++;
// 文件名
String fileName = String.valueOf(shownum);
recordName= fileName+"/";
String path = config.basePath+config.userName+time;
new File(path+"/").mkdirs();
thread.start();
return new ImmutablePair<>(thread.currentThread().getId(),path);
}
public void pause() {
try {
thread.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
*/
/**
* 录屏结束时根据线程ID停止当前录屏的线程,并根据录屏文件的位置生成录屏视频
* @param threadId 线程ID
* @param path 录屏文件的位置
* @throws MovieSaveException
*//*
public void stop(Long threadId,String path) throws MovieSaveException {
int width= (int) config.getRectangle().getWidth();
int height= (int) config.getRectangle().getHeight();
recordSwitch=false;
JpgToAviUtil.convertPicToAvi(config.basePath+config.userName+time+"/",
config.basePath+config.userName+time+".avi",
config.frame,width,height,config.getQuality()
);
*/
/* // 根据线程ID 停止线程
for(Thread thread : setOfThread){
if(thread.getId()==yourThread.getId()){
thread.interrupt();
JpgToAviUtil.convertPicToAvi(path+"/",
path+".avi",
config.frame,width,height,config.getQuality()
);
}
}*//*
}
public void capturOneImg(Rectangle rectangle) {
try {
BufferedImage image = robot.createScreenCapture(rectangle);//捕获制定屏幕矩形区域
serialNum++;
//根据文件前缀变量和文件格式变量,自动生成文件名
Thumbnails.of(image).scale(1).outputQuality(config.getQuality()).toFile(config.basePath+config.userName+time+"/" + String.valueOf(serialNum) + ".jpg");
} catch (IOException e) {
e.printStackTrace();
}
}
*/
/**
* @return java.awt.image.BufferedImage
* @throws
* @description 操纵文字型水印,截图添加水印 todo 待完成
* @params [address, dealerName, latitudeCommaLongitude, date]
*//*
*/
/*
private BufferedImage handleTextWaterMark(String address, String dealerName, String latitudeCommaLongitude, String date) {
final Font font = getSimsunFont();
BufferedImage image = new BufferedImage(800, 320, BufferedImage.TYPE_INT_RGB);
Graphics2D g = image.createGraphics();
image = g.getDeviceConfiguration().createCompatibleImage(800, 320, Transparency.TRANSLUCENT);
int y = 0;
int divider30 = 30;
g = image.createGraphics();
g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g.setColor(Color.red);
g.setFont(font);
if (StringUtils.isNotBlank(address)) {
g.drawString("地址:" + address, 5, y += divider30);
}
if (StringUtils.isNotBlank(dealerName)) {
g.drawString("名称:" + dealerName, 5, y += divider30);
}
if (StringUtils.isNotBlank(latitudeCommaLongitude)) {
g.drawString("经纬度:" + latitudeCommaLongitude, 5, y += divider30);
}
if (StringUtils.isNotBlank(date)) {
g.drawString("拍照时间:" + date, 5, y += divider30);
}
g.dispose();
return image;
}*//*
public Config getConfig() {
return config;
}
public void setConfig(Config config) {
this.config = config;
// 线程沉睡时间
sleepTime = 1000 / config.getFrame();
}
public void init() {
thread = new Thread() {
@Override public void run() {
while (recordSwitch) {
try {
capturOneImg(config.getRectangle());
Thread.sleep(sleepTime);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
}
}
JpgToAviUtil 截图生成视频工具类
import org.jim2mov.core.*;
import org.jim2mov.utils.MovieUtils;
import java.io.File;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Comparator;
import java.util.Date;
public class JpgToAviUtil {
public static void main(String[] args) throws Exception {
String time = new SimpleDateFormat("yyyyMMdd").format(new Date());
// 文件名
String fileName = new SimpleDateFormat("yyyyMMddHHmmss").format(new Date()) + ".avi";
convertPicToAvi("/Users/Desktop/screenRecord/" + "userName20210922140747", fileName, 5, 100, 200,1.0f);
}
*/
/**
* 将图片合成AVI视频
* todo 此方法在windows环境下会报错,未解决,推荐换一种合成视频的方法
* @param jpgDirPath jpg图片文件夹绝对路径
* @param aviFileName 生成的avi视频文件名
* @param fps 每秒帧数
* @param mWidth 视频宽度
* @param mHeight 视频高度
* @param quality 视频压缩率
* @throws MovieSaveException
*//*
public static void convertPicToAvi(String jpgDirPath, String aviFileName, int fps, int mWidth, int mHeight,float quality) throws MovieSaveException {
System.out.println(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss:SSS").format(new Date()) + " 开始生成视频" + aviFileName);
*/
/*String time = new SimpleDateFormat("yyyyMMdd").format(new Date());
System.out.println(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss:SSS").format(new Date()) +" 开始");
//jpgs目录放置JPEG图片,图片文件名为(1.jpg,2.jpg...) 获取指定文件名的所有文件
jpgDirPath = "/Users/liuxudong/Desktop/screenRecord/"+time;*//*
// 获取截图存放位目录中的所有.jpg后缀的文件
final File[] jpgs = new File(jpgDirPath).listFiles(new SuffixFilter(".jpg"));
//对文件名进行排序(本示例假定文件名中的数字越小,生成视频的帧数越靠前)
Arrays.sort(jpgs, new Comparator<File>() {
public int compare(File file1, File file2) {
String numberName1 = file1.getName().replace(".jpg", "");
String numberName2 = file2.getName().replace(".jpg", "");
return new Integer(numberName1) - new Integer(numberName2);
}
});
// 视频名称
DefaultMovieInfoProvider dmip = new DefaultMovieInfoProvider(aviFileName);
// 设置每秒帧数 如果未设置,默认为5
dmip.setFPS(5);
//总帧数
dmip.setNumberOfFrames(jpgs.length);
dmip.setMWidth(mWidth);
dmip.setMHeight(mHeight);
new Jim2Mov(new ImageProvider() {
public byte[] getImage(int frame) {
try {
//设置压缩比
return MovieUtils.convertImageToJPEG((jpgs[frame]), quality);
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
}, dmip, null).saveMovie(MovieInfoProvider.TYPE_AVI_MJPEG);
System.out.println(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss:SSS").format(new Date()) + " 生成视频" + aviFileName + "结束");
}
}
使用javacv将图片合成MP4视频(windows环境下也可用,推荐用此方法)
引入maven依赖(下载比较慢,需等待)
<!--JAVA使用javacv实现图片合成短视频,相关JAR包-->
<dependency>
<groupId>org.bytedeco</groupId>
<artifactId>javacv</artifactId>
<version>1.5.2</version>
</dependency>
<dependency>
<groupId>org.bytedeco</groupId>
<artifactId>javacv-platform</artifactId>
<version>1.5.2</version>
</dependency>
/**
* 将图片合成MP4视频
* @param mp4SavePath 生成的视频存放位置
* @param imgPath jpg图片文件夹绝对路径
* @param width 视频宽度
* @param height 视频高度
* @param quality 视频压缩率
* @throws FrameRecorder.Exception
*/
public static void createMp4(String mp4SavePath, String imgPath, int width, int height, float quality) throws FrameRecorder.Exception {
System.out.println(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss:SSS").format(new Date()) + " 生成视频" + mp4SavePath + "开始");
//读取所有图片
final File[] jpgs = new File(imgPath).listFiles(new SuffixFilter(".jpg"));
Map<Integer, File> imgMap = new HashMap<Integer, File>();
for (File imgFile : jpgs) {
String substring = imgFile.getName().substring(0, imgFile.getName().lastIndexOf(".")).trim();
int i =(int)Long.valueOf(substring).intValue();
imgMap.put(i, imgFile);
}
Object[] objects = imgMap.keySet().toArray();
Arrays.sort(objects);
//视频宽高最好是按照常见的视频的宽高 16:9 或者 9:16
FFmpegFrameRecorder recorder = new FFmpegFrameRecorder(mp4SavePath, width, height);
//设置视频编码层模式
recorder.setVideoCodec(avcodec.AV_CODEC_ID_H264);
//设置视频为25帧每秒
recorder.setFrameRate(25);
//设置视频图像数据格式
recorder.setPixelFormat(avutil.AV_PIX_FMT_YUV420P);
recorder.setFormat("mp4");
recorder.setAudioQuality(quality);
try {
recorder.start();
Java2DFrameConverter converter = new Java2DFrameConverter();
//根据文件数量定义视频时长
for (Object object : objects) {
File file = imgMap.get(object);
BufferedImage read = ImageIO.read(file);
//6:控制的是快进的速率/倍数
for (int i = 0; i < 6; i++) {
recorder.record(converter.getFrame(read));
}
}
} catch (Exception e) {
e.printStackTrace();
} finally {
//最后一定要结束并释放资源
recorder.stop();
recorder.release();
}
System.out.println(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss:SSS").format(new Date()) + " 生成视频" + mp4SavePath + "结束");
}
SuffixFilter 文件后缀名过滤
import java.io.File;
import java.io.FilenameFilter;
public class SuffixFilter implements FilenameFilter {
private String suffix;
public SuffixFilter(String suffix) {
super();
this.suffix = suffix;
}
public boolean accept(File dir, String name) {
return name.endsWith(suffix);
}
}
测试录屏接口
/**
* 调用录屏接口 开启单独用于录屏的线程
* @param userName 用户名
* @return
*/
@Override
public Integer testScreenRecord(String userName) {
// 两个线程的线程池
ExecutorService executor = Executors.newFixedThreadPool(2);
try {
System.out.println(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss:SSS").format(new Date()) +" 开始屏幕录像");
// jdk1.8之前的实现方式
CompletableFuture<String> future = CompletableFuture.supplyAsync(new Supplier<String>() {
@Override
public String get() {
try {
ScreenRecording screenRecordTest = new ScreenRecording();
Config config=new Config();
config.setBasePath(screenRecordFolder);
config.setQuality(quality);
config.setScale(scale);
config.setRectangle(config.getScreenFull());
config.setUserName(userName);
System.out.println("config:"+config.toString());
screenRecordTest.setConfig(config);
ImmutablePair<Long,String> result = screenRecordTest.start();
// System.out.println("线程启动:"+result.getLeft());
// 模拟录屏20秒,可单独根据当前录屏的线程IDresult.getLeft()来停止当前录屏
Thread.sleep(20000);
screenRecordTest.stop(result.getLeft(),result.getRight());
} catch (AWTException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (MovieSaveException e) {
e.printStackTrace();
}
System.out.println(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss:SSS").format(new Date()) +" 屏幕录像结束");
return "1";
}
}, executor);
//采用lambada的实现方式
future.thenAccept(e -> System.out.println(e + " ok"));
} catch (Exception e) {
e.printStackTrace();
} finally {
executor.shutdown();
}
return 1;
}
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>cn.xx.xx</groupId>
<artifactId>screenRecord</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<dependencies>
<!-- https://mvnrepository.com/artifact/com.jcraft/jsch -->
<dependency>
<groupId>com.jcraft</groupId>
<artifactId>jsch</artifactId>
<version>0.1.55</version>
</dependency>
<!-- https://mvnrepository.com/artifact/ws.schild/jave-all-deps -->
<dependency>
<groupId>ws.schild</groupId>
<artifactId>jave-all-deps</artifactId>
<version>3.1.1</version>
</dependency>
<!-- https://mvnrepository.com/artifact/ws.schild/jave-core -->
<dependency>
<groupId>ws.schild</groupId>
<artifactId>jave-core</artifactId>
<version>3.1.1</version>
</dependency>
<!-- https://mvnrepository.com/artifact/ws.schild/jave-nativebin-linux-arm64 -->
<dependency>
<groupId>ws.schild</groupId>
<artifactId>jave-nativebin-linux-arm64</artifactId>
<version>3.1.1</version>
</dependency>
<!-- https://mvnrepository.com/artifact/ws.schild/jave-nativebin-win64 -->
<dependency>
<groupId>ws.schild</groupId>
<artifactId>jave-nativebin-win64</artifactId>
<version>3.1.1</version>
</dependency>
<!-- https://mvnrepository.com/artifact/ws.schild/jave-nativebin-linux32 -->
<dependency>
<groupId>ws.schild</groupId>
<artifactId>jave-nativebin-linux32</artifactId>
<version>3.1.1</version>
</dependency>
<!-- https://mvnrepository.com/artifact/ws.schild/jave-nativebin-linux64 -->
<dependency>
<groupId>ws.schild</groupId>
<artifactId>jave-nativebin-linux64</artifactId>
<version>3.1.1</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
<version>5.2.12.RELEASE</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.apache.commons/commons-lang3 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.11</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>RELEASE</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.fusesource</groupId>
<artifactId>sigar</artifactId>
<version>1.6.4</version>
</dependency>
<!--<dependency>
<groupId>com.Jim2mov</groupId>
<artifactId>Jim2mov</artifactId>
<version>1.0.0</version>
<!--<scope>system</scope>-->
<!--<systemPath>${basedir}/src/main/resources/lib/Jim2mov.jar</systemPath>
</dependency>
<dependency>
<groupId>com.mediaplayer</groupId>
<artifactId>mediaplayer</artifactId>
<version>1.0.0</version>
<!--<scope>system</scope>
<systemPath>${basedir}/src/main/resources/lib/mediaplayer.jar</systemPath>
</dependency>-->
<dependency>
<groupId>com.jmf</groupId>
<artifactId>jmf</artifactId>
<version>1.0.0</version>
<!--<scope>system</scope>
<systemPath>${basedir}/src/main/resources/lib/jmf.jar</systemPath>-->
</dependency>
<dependency>
<groupId>net.coobird</groupId>
<artifactId>thumbnailator</artifactId>
<version>0.4.8</version>
</dependency>
<!--JAVA使用javacv实现图片合成短视频,相关JAR包-->
<dependency>
<groupId>org.bytedeco</groupId>
<artifactId>javacv</artifactId>
<version>1.5.2</version>
</dependency>
<dependency>
<groupId>org.bytedeco</groupId>
<artifactId>javacv-platform</artifactId>
<version>1.5.2</version>
</dependency>
</dependencies>
<build>
<finalName>NFPS_SCREEN_RECORD</finalName>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<version>3.3.0</version>
<configuration>
<appendAssemblyId>false</appendAssemblyId>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
<archive>
<manifest>
<!--此处指定main方法入口的class-->
<mainClass>screenRecordCopy.ScreenRecordTest</mainClass>
</manifest>
</archive>
</configuration>
<executions>
<execution>
<id>make-assembly</id>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>