All Downloads are FREE. Search and download functionalities are using the official Maven repository.

cn.novelweb.video.recording.ScreenRecorder Maven / Gradle / Ivy

package cn.novelweb.video.recording;

import cn.hutool.core.thread.ThreadFactoryBuilder;
import cn.hutool.core.thread.ThreadUtil;
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.bytedeco.ffmpeg.global.avcodec;
import org.bytedeco.javacv.FFmpegFrameRecorder;
import org.bytedeco.javacv.FrameRecorder;
import org.bytedeco.javacv.Java2DFrameConverter;

import javax.sound.sampled.AudioFormat;
import javax.sound.sampled.AudioSystem;
import javax.sound.sampled.DataLine;
import javax.sound.sampled.TargetDataLine;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.File;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.ShortBuffer;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

/**
 * 

屏幕录制程序

*

录屏软件

*

2019-12-10 14:19

* * @author Dai Yuanchuan **/ @Slf4j @JsonInclude(JsonInclude.Include.NON_NULL) public class ScreenRecorder { /** * 从系统中获取图像、屏幕分辨率、屏幕色彩模型、全屏的时候获得屏幕大小等等 */ private static final Toolkit TOOLKIT = Toolkit.getDefaultToolkit(); /** * 帧记录器 * 视频类 */ private static FFmpegFrameRecorder recorder; /** * 用于访问许多音频格式和通道配置常量 */ private static AudioFormat audioFormat; /** * 用于生成本机系统输入事件 * 这里主要是用这个类来创建屏幕截图 */ private static Robot robot; /** * 提供特定于数据行的附加信息 * 这些信息包括: * 数据线支持的音频格式 * 内部缓冲区的最小和最大尺寸 */ private static DataLine.Info dataLineInfo; /** * 打开并开始捕获音频 * 对所选音频设备进行更多的控制 */ private static TargetDataLine line; /** * 设置一个用于屏幕录制的线程池 */ private static ScheduledThreadPoolExecutor screenThreadPool; /** * 设置一个用于抓取音频的线程池 */ private static ScheduledThreadPoolExecutor audioThreadPool; /** * 记录暂停时间的参数 */ private long pauseTime = 0; /** * 需要录制的屏幕宽度 * 默认为当前屏幕宽度 */ private int width = TOOLKIT.getScreenSize().width; /** * 需要录制的屏幕高度 * 默认为当前屏幕高度 */ private int height = TOOLKIT.getScreenSize().height; /** * 视频编解码器、视频的压缩方式、视频解码 * 默认id为:12 */ private int videoCodec = avcodec.AV_CODEC_ID_MPEG4; /** * 音讯编解码器;音频编码格式;音频编解码; * 默认id为:86018 */ private int audioCodec = avcodec.AV_CODEC_ID_AAC; /** * 确定视频格式 * 可以是flv、mov、mp4、m4a、3gp、3g2、mj2、h264、ogg * 默认是:mp4 */ private String format = "mp4"; /** * 音频采样率是指录音设备在一秒钟内对声音信号的采样次数 * 采样频率越高声音的还原就越真实越自然 * 在当今的主流采集卡上,采样频率一般共分为11025Hz、22050Hz、24000Hz、44100Hz、48000Hz五个等级 * 8000Hz一般为电话所用采样率,对于人的说话已经足够 * 11025Hz一般为AM调幅广播的声音品质 * 22050Hz和24000Hz一般为FM调频广播所用采样率 * 32000Hz一般为miniDV、数码视频camcorder、DAT (LP mode)所用采样率 * 44100Hz一般为音频(CD、VCD、超级VCD、MP3)所用采样率 * 48000Hz一般为miniDV、数字电视、DVD、DAT、电影和专业音频所用的数字声音所用采样率 * 这个数值也要看你的声卡可以支持的数值 * 默认是:48000Hz */ private int sampleRate = 48000; /** * 帧频,帧速率,是指每秒钟刷新的图片的帧数 * 也可以理解为图形处理器每秒钟能够刷新几次 * 对影片内容而言,帧速率指每秒所显示的静止帧格数 * 要生成平滑连贯的动画效果,帧速率一般不小于8 * 而电影的帧速率为24fps,但如果你只是录屏5FPS也是可以的 * 如果太高的话会出现声音和画面对不上的情况 * 默认是:10FPS */ private int frameRate = 10; /** * 关键帧间隔,一般与帧率相同或者是视频帧率的两倍 * 默认是:frameRate * 2 */ private int gopSize = this.frameRate * 2; /** * 视频质量 * 默认是:0最高质量 */ private int videoQuality = 0; /** * 音频的质量 * 默认是:0最高质量 */ private int audioQuality = 0; /** * 设定固定视频比特率 * 2000kb/s,720P视频的合理比特率范围 * 默认是:2000000 */ private int videoBitrate = 2000000; /** * 设置固定的音频比特率 * 默认是:192000 192Kbps */ private int audioBitrate = 192000; /** * 设置内容速率因子,这是一个x264的动态比特率参数 * 它能够在复杂场景下(使用不同比特率,即可变比特率)保持视频质量 * 比特率越高视频越清晰,视频体积也会变大,需要根据实际选择合理范围 * 设置固定位元率系数,可以设置为0 * 默认是:25 */ private String crf = "25"; /** * 权衡 视频质量 和 编码速度 的值 * 参考值:ultrafast(终极快),superfast(超级快),veryfast(非常快),faster(很快), * fast(快),medium(中等),slow(慢),slower(很慢),veryslow(非常慢) * ultrafast(终极快)提供最少的压缩(低编码器CPU)和最大的视频流大小 * veryslow(非常慢)提供最佳的压缩(高编码器CPU)的同时降低视频流的大小 * 默认是:slow */ private String preset = "slow"; /** * 设置像素格式 * YUV420一帧的大小size=width×height×1.5 Byte * 默认是:0 AV_PIX_FMT_YUV420P */ private int pixelFormat = 0; /** * 设置音频的声道 * 可选值为:2(立体声);1(单声道);0(无音频); * 默认是:2 双通道(立体声) */ private int audioChannels = 2; /** * 录取的视频需要保存到的目录 * 这个是文件夹,文件目录,不是文件 * 默认是:当前系统工作目录 */ private String saveTo = System.getProperty("user.dir"); /** * 录屏的同时是否需要录音 * 需要有录音设备的支持 * 默认是:true需要 */ private boolean isAudioRecorder = true; /** * 设置核心线程池的大小 * 默认是你当前CPU的核心数的一半 * 如果你是单核CPU,线程池大小为1 */ private int corePoolSize = Math.max(1, Runtime.getRuntime().availableProcessors() / 2); public int getWidth() { return width; } public void setWidth(int width) { this.width = width; } public int getHeight() { return height; } public void setHeight(int height) { this.height = height; } public int getVideoCodec() { return videoCodec; } public void setVideoCodec(int videoCodec) { this.videoCodec = videoCodec; } public int getAudioCodec() { return audioCodec; } public void setAudioCodec(int audioCodec) { this.audioCodec = audioCodec; } public String getFormat() { return format; } public void setFormat(String format) { this.format = format; } public int getSampleRate() { return sampleRate; } public void setSampleRate(int sampleRate) { this.sampleRate = sampleRate; } public int getFrameRate() { return frameRate; } public void setFrameRate(int frameRate) { this.frameRate = frameRate; } public int getGopSize() { return gopSize; } public void setGopSize(int gopSize) { this.gopSize = gopSize; } public int getVideoQuality() { return videoQuality; } public void setVideoQuality(int videoQuality) { this.videoQuality = videoQuality; } public int getAudioQuality() { return audioQuality; } public void setAudioQuality(int audioQuality) { this.audioQuality = audioQuality; } public int getVideoBitrate() { return videoBitrate; } public void setVideoBitrate(int videoBitrate) { this.videoBitrate = videoBitrate; } public int getAudioBitrate() { return audioBitrate; } public void setAudioBitrate(int audioBitrate) { this.audioBitrate = audioBitrate; } public String getCrf() { return crf; } public void setCrf(String crf) { this.crf = crf; } public String getPreset() { return preset; } public void setPreset(String preset) { this.preset = preset; } public int getPixelFormat() { return pixelFormat; } public void setPixelFormat(int pixelFormat) { this.pixelFormat = pixelFormat; } public int getAudioChannels() { return audioChannels; } public void setAudioChannels(int audioChannels) { this.audioChannels = audioChannels; } public String getSaveTo() { return saveTo; } public void setSaveTo(String saveTo) { this.saveTo = saveTo; } public boolean isAudioRecorder() { return isAudioRecorder; } public void setAudioRecorder(boolean audioRecorder) { isAudioRecorder = audioRecorder; } public int getCorePoolSize() { return corePoolSize; } public void setCorePoolSize(int corePoolSize) { this.corePoolSize = corePoolSize; } /** * 初始化状态代号 * 用来与当前状态做判断的 */ private static final String INIT = "init"; /** * 开始录制时的状态代号 * 用来与当前状态做判断的 */ private static final String START = "start"; /** * 暂停录制时的状态代号 */ private static final String PAUSE = "pause"; /** * 结束录制时的状态代号 */ private static final String STOP = "stop"; /** * 用来记录当前的运行时的状态 * 默认为:初始化状态init */ private static String current = INIT; /** * 初始化格式、编码器、比特率、采样率;分配AVPacket空间 * 每次开启录制前需要且只能调用一次初始化方法 * * @param file 保存的录屏文件的文件名(不要到后缀,这里的后缀默认为.mp4) */ private void init(Object file) { String fileName = this.saveTo + File.separatorChar + file.toString() + ".mp4"; // 指定文件名、分辨率 recorder = new FFmpegFrameRecorder(fileName, this.width, this.height); recorder.setVideoCodec(this.videoCodec); recorder.setFormat(this.format); recorder.setSampleRate(this.sampleRate); recorder.setFrameRate(this.frameRate); recorder.setVideoQuality(this.videoQuality); recorder.setVideoOption("crf", this.crf); recorder.setVideoOption("preset", this.preset); recorder.setVideoBitrate(this.videoBitrate); recorder.setPixelFormat(this.pixelFormat); recorder.setAudioChannels(this.audioChannels); recorder.setAudioOption("crf", "0"); recorder.setAudioQuality(this.audioQuality); recorder.setAudioCodec(this.audioCodec); recorder.setGopSize(this.gopSize); recorder.setAudioBitrate(this.audioBitrate); // 重定向当前状态为 start current = START; // 初始化Robot try { robot = new Robot(); } catch (AWTException e) { log.error("初始化屏幕截图异常:" + e.getMessage()); } // 创建并设置编码器、打开编码器、申请必要的编码缓存区 try { recorder.start(); } catch (FrameRecorder.Exception e) { log.error("初始化编码器异常:" + e.getMessage()); } } /** * 开始录屏 * 默认使用当前系统的时间 */ public void start() { start(String.valueOf(System.currentTimeMillis())); } /** * 开始录屏 */ public void start(String file) { if (StringUtils.isBlank(file)) { file = String.valueOf(System.currentTimeMillis()); } if (current.equals(INIT)) { init(file); } // 判断当前状态是否等于 start if (!current.equals(START) && !current.equals(PAUSE)) { return; } // 重定向当前状态为:stop current = STOP; // 指定开启录音 if (isAudioRecorder) { ThreadUtil.execute(this::soundRecorder); } // 开启一个录屏线程 screenThreadPool = new ScheduledThreadPoolExecutor(this.corePoolSize, new ThreadFactoryBuilder().build()); // 截屏的大小 final Rectangle rectangle = new Rectangle(this.width, this.height); if (pauseTime == 0) { pauseTime = System.currentTimeMillis(); } long startTime = System.currentTimeMillis(); screenThreadPool.scheduleAtFixedRate(() -> { try { // 创建一个截屏的图片 BufferedImage screenCapture = robot.createScreenCapture(rectangle); // 声明一个BufferedImage用重绘截图 // BufferedImage.TYPE_3BYTE_BGR表示具有8位RGB颜色分量的图像 // 对应到一个窗口风格的BGR颜色模型与颜色蓝色,绿色,和红色存储在3个字节中 BufferedImage videoImg = new BufferedImage(this.width, this.height, BufferedImage.TYPE_3BYTE_BGR); // 创建videoImg的Graphics2D Graphics2D videoGraphics = videoImg.createGraphics(); videoGraphics.setRenderingHint(RenderingHints.KEY_DITHERING, RenderingHints.VALUE_DITHER_DISABLE); videoGraphics.setRenderingHint(RenderingHints.KEY_COLOR_RENDERING, RenderingHints.VALUE_COLOR_RENDER_SPEED); videoGraphics.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_SPEED); // 重绘截图 videoGraphics.drawImage(screenCapture, 0, 0, null); Java2DFrameConverter java2dConverter = new Java2DFrameConverter(); org.bytedeco.javacv.Frame frame = java2dConverter.convert(videoImg); long videoTs = 1000L * (System.currentTimeMillis() - startTime - (System.currentTimeMillis() - pauseTime)); if (videoTs > recorder.getTimestamp()) { recorder.setTimestamp(videoTs); } // 录制视频 recorder.record(frame); frame.clone(); // 资源释放 videoGraphics.dispose(); videoImg.flush(); screenCapture.flush(); } catch (Exception e) { log.error("视频录制线程异常:" + e.getMessage()); } }, 1000 / this.frameRate, 1000 / this.frameRate, TimeUnit.MILLISECONDS); System.gc(); } /** * 抓取声音、声音的录制 */ private void soundRecorder() { audioFormat = new AudioFormat(this.sampleRate, 16, this.audioChannels, true, false); dataLineInfo = new DataLine.Info(TargetDataLine.class, audioFormat); try { line = (TargetDataLine) AudioSystem.getLine(dataLineInfo); line.open(audioFormat); line.start(); final int sampleRate = (int) audioFormat.getSampleRate(); final int numChannels = audioFormat.getChannels(); // 开始初始化音频缓冲区 int audioBufferSize = sampleRate * numChannels; final byte[] audioBytes = new byte[audioBufferSize]; // 初始化音频的线程池 audioThreadPool = new ScheduledThreadPoolExecutor(this.corePoolSize, new ThreadFactoryBuilder().build()); audioThreadPool.scheduleAtFixedRate(() -> { try { // 开始读取行 int nBytesRead = line.read(audioBytes, 0, line.available()); // 由于在AudioFormat中指定了16位,因此需要将读取的byte[]转换为short[] int nSamplesRead = nBytesRead / 2; // 初始化短[]数组 short[] samples = new short[nSamplesRead]; // 将short[]封装到一个ShortBuffer中,并将它传递给recordSamples ByteBuffer.wrap(audioBytes).order(ByteOrder.LITTLE_ENDIAN).asShortBuffer().get(samples); ShortBuffer sBuff = ShortBuffer.wrap(samples, 0, nSamplesRead); // 开始记录样本 recorder.recordSamples(sampleRate, numChannels, sBuff); } catch (org.bytedeco.javacv.FrameRecorder.Exception e) { log.error("声音抓取线程异常:" + e.getMessage()); } }, 1000 / this.frameRate, 1000 / this.frameRate, TimeUnit.MILLISECONDS); } catch (Exception e) { log.error("声音抓取方法体异常:" + e.getMessage()); } } /** * 暂停录制 */ public void pause() { // 当前正在录屏 if (!current.equals(STOP)) { return; } current = PAUSE; // 关闭用于录屏的线程 if (screenThreadPool != null) { screenThreadPool.shutdown(); } // 如果同时开启了录音,还需要关闭用于录音的线程 stopAudio(); screenThreadPool = null; pauseTime = System.currentTimeMillis(); System.gc(); } /** * 停止录制,同时生成录屏文件 */ public void stop() { // 当前正在录屏 if (!current.equals(STOP) && !current.equals(PAUSE)) { return; } current = INIT; try { if (screenThreadPool != null) { screenThreadPool.shutdownNow(); } recorder.stop(); recorder.release(); recorder.close(); recorder = null; // 如果同时开启了录音,还需要关闭用于录音的线程 stopAudio(); screenThreadPool = null; pauseTime = 0; System.gc(); } catch (Exception e) { log.error("stop方法体执行异常:" + e.getMessage()); } } /** * 关闭用于录音的线程 */ private void stopAudio() { if (this.isAudioRecorder) { if (audioThreadPool != null) { audioThreadPool.shutdown(); } if (line != null) { line.stop(); line.close(); } dataLineInfo = null; audioFormat = null; line = null; audioThreadPool = null; } } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy