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>