news 2026/9/16 16:56:12

JavaFX + AWT Robot + JavaCV:桌面录屏录音工具的实现与编码实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
JavaFX + AWT Robot + JavaCV:桌面录屏录音工具的实现与编码实践

简介:一款基于JavaFX的桌面录屏录音软件完整源码,面向熟悉Java语法、希望进阶桌面应用与多媒体开发的读者,用来解决录屏、录音、暂停、播放和MP4导出的完整流程实现。项目通过Robot类定时抓取屏幕图像,使用Java声音API采集麦克风音频,并借助javacv、ffmpeg等第三方库完成H.264视频与AAC音频编码,最终封装为MP4文件,是理解JavaFX界面与AWT底层屏幕捕获协同工作的典型示例。压缩包共48个文件,主要包含Java源码、编译后的class文件、运行所需的依赖jar包、界面预览png图片及工程配置文件,压缩后约14.93MB,目录划分清楚,方便按模块查找。源码中除了核心的录制、音频采集、播放和编码模块,还涉及多线程任务、JavaFX事件驱动和异常处理,读者可以从主入口逐步看懂屏幕帧获取、音频同步和暂停恢复的实现细节。目前已有6258人浏览学习,对于想自己动手开发桌面录屏工具,或希望掌握Java多媒体处理与第三方编码库集成的开发者,是一份参考价值很高的实践案例。

1. 从捡起一个录屏需求说起

接到"做一个桌面录屏录音工具"这个需求时,如果你第一反应是去JavaFX的API里找屏幕捕获能力,多半会扑个空。JavaFX把精力花在UI渲染和事件模型上,压根没有提供抓屏的接口——真正能拿到屏幕像素的,是AWT里那个出道二十多年的Robot类。所以这个项目最反直觉的一点在于:一套UI是现代JavaFX,底层采集却是"老古董"AWT,然后再用JavaCV把帧序列编码成MP4。这恰是Java桌面多媒体开发的典型组合,适合想搞清"录屏到底怎么把帧、音频、编码器串起来"的人。下文按选型拆解、抓帧录音、编码封装、播放排错的顺序,完整过一遍能落地的实现。

2. 技术选型:为什么是 AWT + JavaCV,而不是纯 JavaFX 方案

2.1 录制链路里四个模块的职责边界

录屏录音软件拆开看是四条独立管线:界面控制、帧采集、音频采集、编码封装。界面这层用JavaFX没有争议,Application基类、Stage窗口、Scene场景树,事件驱动天然适合开始/暂停/停止这类操作。关键是后三层:帧采集在Java标准库内只有java.awt.Robot一个选择,它能把屏幕指定区域截成BufferedImage;音频采集标准库提供javax.sound.sampled,通过TargetDataLine从麦克风读PCM数据;编码封装则绕不开第三方库,MP4容器和H.264压缩标准库一个都不支持,项目里引入的JavaCV正是干这个的。

音频采集这条线容易被人忽略,但javax.sound.sampled在暂停恢复的场景下有个天然优势:TargetDataLineread()方法按字节流读取,暂停时只要停止调用read(),缓冲区的数据不会丢也不会跳秒,恢复时继续读就能接上。如果用第三方音频库,反而要额外处理缓冲区和时间戳的拼接逻辑。

2.2 JavaCV 封装了什么,四个 jar 各自的作用

项目libs目录下那几个jar,javacv.jar是JavaCV的核心API,ffmpeg.jar是FFmpeg的Java绑定,ffmpeg-platform.jar带一整套跨平台native库,ffmpeg-windows-x86_64.jar则把Windows 64位系统对应的FFmpeg动态库单独抽出来。说直白点,JavaCV就是FFmpeg的Java门面,你不需要用JNI去写C代码调用FFmpeg,而是直接操作FFmpegFrameRecorderFFmpegFrameGrabber这两个类。

javacpp.jar这层容易被忽略,它是JavaCPP的底层支撑,负责在Java和C++之间搬数据。没有它,JavaCV调不动FFmpeg的native库。理解这个依赖链对排查环境问题有用——哪天程序启动报UnsatisfiedLinkError,九成是native库没加载上,而不是你的业务代码出问题。

2.3 为什么不用纯FFmpeg命令行,也不用JCodec

外部调用FFmpeg进程(ProcessBuilder拼命令行)是另一种做法,但"进程间通信+文件中转"意味着每一帧都要写临时文件,而且暂停函数要做进程挂起而不是简单的线程挂起。JCodec知名度不如JavaCV,H.264编码支持也不完整,遇到"暂停后继续录制"这种操作基本没有现成方案。JavaCV的方案把编码器内嵌到进程内,帧数据从BufferedImage直接转Frame,内存操作,暂停逻辑只在业务层做,可控性强得多。

3. 帧采集与音频录制的并发实现

3.1 Robot 抓帧 + 定时器,帧率怎么定

RobotcreateScreenCapture方法拿到的是BufferedImage,但JavaCV的编码器要的是Frame,中间需要一次类型转换。标准做法是Java2DFrameConverterconvert()方法,把BufferedImage变成FFmpeg认识的Frame。帧率直接决定文件大小和流畅度,录屏软件一般取15到30 FPS,PPT讲解类内容15 FPS足够,操作演示类建议30 FPS。

// 定时任务里执行抓帧和编码 private void captureFrame() { BufferedImage image = robot.createScreenCapture(screenRect); Frame frame = converter.convert(image); try { recorder.record(frame); } catch (FFmpegFrameRecorder.Exception e) { e.printStackTrace(); } } // 主线程启动定时任务,2秒延迟后开始 ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(); scheduler.scheduleAtFixedRate(this::captureFrame, 2000, 1000 / FPS, TimeUnit.MILLISECONDS);

createScreenCapture需要传入一个Rectangle对象,表示捕获区域。如果要全屏录制,用GraphicsEnvironment.getLocalGraphicsEnvironment().getMaximumWindowBounds()拿整个虚拟屏幕的边界,这样能覆盖多显示器场景。scheduleAtFixedRate的第三个参数1000 / FPS就是帧间隔,用固定速率调度比scheduleWithFixedDelay更合适——后者是"上一帧执行完再等一个间隔",帧率会越来越低。定时器线程要单独建,不要占UI线程,否则界面一卡,抓帧和编码全部停摆。

3.2 TargetDataLine 录音通道的参数校对

javax.sound.sampled的音频采集比想象中简单,核心就是AudioFormatTargetDataLine。格式参数必须和编码器预期匹配:采样率44100 Hz是CD音质,48 kHz是视频常用标准(推荐后者),16位量化精度,单声道录人声,双声道保留环境声。JavaCV里设置音频参数时,这几个值必须和AudioFormat一致,不然后期record会报采样率不匹配的错。

AudioFormat format = new AudioFormat(48000, 16, 2, true, true); DataLine.Info info = new DataLine.Info(TargetDataLine.class, format); TargetDataLine microphone = (TargetDataLine) AudioSystem.getLine(info); microphone.open(format); microphone.start(); byte[] audioBuffer = new byte[8192]; while (isRecording) { int bytesRead = microphone.read(audioBuffer, 0, audioBuffer.length); recorder.recordSamples(48000, 2, 16, audioBuffer, bytesRead); }

recordSamplesFFmpegFrameRecorder提供的方法,直接喂PCM裸数据,不用手动封装成FramebytesRead是实际读到的字节数,音频设备IO不稳定时,返回值小于audioBuffer.length是正常现象,必须用返回值而不是数组长度。八个参数的含义分别是:采样率、声道数、位深、样本数据、有效长度。

注意:recordrecordSamples不能混用在一个录制周期里,否则编码器内部的状态机可能错乱。视频走record(Frame),音频固定走recordSamples,各走各的通道。

3.3 暂停与恢复的正确打开方式

暂停是整个项目里最容易写崩的地方。刚接触录制功能的人很容易想到"暂停就停线程,恢复就重启线程",但这样写,recorder内部的时间戳就断了——编码器认为你新建了一条视频流,而不是在原来那条上续录。FFmpegFrameRecorderrecord方法会在内部累计PTS(显示时间戳),线程停下再恢复,PTS断档,播放时画面会卡住几秒甚至跳帧。

我一般用标志位加等待策略,而不是销毁线程:

private volatile boolean isPaused = false; private final Object pauseLock = new Object(); private void captureFrame() { synchronized (pauseLock) { while (isPaused) { try { pauseLock.wait(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); return; } } } // 继续执行抓帧和编码 } public void pause() { isPaused = true; } public void resume() { synchronized (pauseLock) { isPaused = false; pauseLock.notifyAll(); } }

wait/notifyscheduleAtFixedRate里有个好处:暂停时线程还在,只是阻塞在wait()上,恢复后直接从下一帧开始,recorder的时间轴是连续的。录制时长如果要用当前时间减开始时间,暂停时要把暂停的累计时间减掉,否则恢复后文件时长比实际多一段。

4. JavaCV 编码管线:帧数据怎么变成 mp4 文件

4.1 FFmpegFrameRecorder 关键参数与设备兼容性

编码器初始化参数直接决定输出MP4的可用性,这段配置踩过的坑最多。

FFmpegFrameRecorder recorder = new FFmpegFrameRecorder( outputFile, screenWidth, // 视频宽 screenHeight, // 视频高 2 // 声道数,纯录屏可传 0 ); recorder.setFormat("mp4"); recorder.setVideoCodec(avcodec.AV_CODEC_ID_H264); recorder.setVideoBitrate(3000 * 1000); // 3 Mbps recorder.setFrameRate(FPS); recorder.setAudioCodec(avcodec.AV_CODEC_ID_AAC); recorder.setSampleRate(48000); recorder.setAudioBitrate(192 * 1000); // 192 kbps recorder.start();

视频编码器选了AV_CODEC_ID_H264,这是目前兼容性最好的选择,Windows Media Player和手机相册都能播。setVideoBitrate的值按分辨率估算:1080P屏幕建议4000到8000 kbps,720P给2000到4000够用。码率给太高文件膨胀,给太低文字边缘全是水波纹。音频编码器用AAC,比MP3在低码率下保留的人声更干净。

注意:JavaCV的H.264编码器默认是libx264软件编码,CPU占用会明显偏高。如果目标机器性能差,可以把视频编码器换成AV_CODEC_ID_MPEG4,兼容性稍差但编码速度快不少。

4.2 音视频交错时的 PTS 对齐逻辑

MP4容器的音视频交错(interleave)靠PTS完成,JavaCV的FFmpegFrameRecorder内部会处理大部分对齐工作,但前提是两路时间戳的基准一致。视频帧按1000/FPS毫秒递增,音频按采样率递增,编码器会自动换算成统一的时间基。需要注意的是start()之后不要立刻record,最好先睡200到300毫秒,让编码器完成内部缓冲区的初始化,否则前几帧容易丢。

这个细节在暂停恢复后重新编码时再踩一次:恢复后的第一帧,PTS如果从零开始,编码器会认为这是新视频段,输出文件在播放器里表现为"进度条不走但画面在动"。JavaCV的record方法会自动延续之前的PTS计数,前提是recorder对象没有被重新start()

4.3 退出时资源释放的正确顺序

录制结束的资源释放顺序有讲究:先停音频流,再停定时任务,最后调recorder.close()

public void stop() { isPaused = false; isRecording = false; scheduler.shutdownNow(); microphone.stop(); microphone.close(); try { recorder.close(); } catch (FFmpegFrameRecorder.Exception e) { e.printStackTrace(); } }

microphone.stop()microphone.close()之间的顺序不能颠倒,stop()是停止数据采集,close()是释放设备句柄。如果先close()stop(),部分Windows声卡驱动会抛IllegalStateExceptionrecorder.close()会flush编码器缓冲区并写入MP4的moov box,这个操作可能耗时几百毫秒到几秒,取决于录制的时长和码率,所以stop()必须在后台线程调用,不能在JavaFX的Platform.runLater里同步执行,否则界面会卡住。

5. JavaFX 界面与录制状态机的对接

5.1 Platform.runLater 与后台录制线程的协作

JavaFX的UI线程不能阻塞,录制和编码必须跑在后台线程。但界面的按钮状态(开始/暂停/停止切换)要随录制状态变化,这就涉及线程间通信。Platform.runLater是把界面更新操作扔回UI线程的经典手段,注意别在录制循环里频繁调用它——每帧都调runLater去刷新状态Label,UI线程会被回调淹没,录出来的视频反而因为UI卡顿而掉帧。

状态机建议只维护四个状态:IDLERECORDINGPAUSEDSTOPPED,界面根据状态切换按钮可点性。开始按钮在IDLE时可用,暂停按钮只在RECORDING时可用,恢复按钮只在PAUSED时可用,停止按钮只要不是IDLE就能点。

public void startRecording() { if (recorderState != State.IDLE) return; recorderState = State.RECORDING; startTime = System.currentTimeMillis(); recordingTask = new RecordingTask(); Thread thread = new Thread(recordingTask, "screen-recorder-thread"); thread.setDaemon(true); thread.start(); Platform.runLater(this::updateButtonState); }

RecordingTask是一个Runnable,内部执行的是JavaCV编码循环。捕获区域和音频格式都在这之前就配置好,这样任务线程启动后能立即进入captureFrame循环。setDaemon(true)保证应用关窗口时录制线程不会阻止JVM退出,但这对录制类应用是把双刃剑——窗口关了线程直接死掉,MP4文件没写完。所以窗口关闭事件里必须显示调用stopRecording(),不能依赖守护线程的自动清理。

5.2 MediaPlayer 播放验证:录制结果能不能播

录制完的MP4能否在应用内直接播放,取决于JavaFX的MediaPlayer支持情况。JavaFX的MediaPlayer仅支持H.264/AAC编码的MP4,以及部分MP3、WAV格式。如果你往里放一个MPEG4 Part 2编码的MP4,播放器只会报MediaException: MEDIA_UNSUPPORTED,不会给你任何"编码格式不对"的提示——这是踩坑最容易懵的地方。

播放的核心代码很短:

Media media = new Media(Paths.get(outputFilePath).toUri().toString()); mediaPlayer = new MediaPlayer(media); mediaView.setMediaPlayer(mediaPlayer); mediaPlayer.setAutoPlay(true);

代码就这三行,但MediaView要想正确显示画面,必须添加到布局容器里,而且要设置合适的fitWidthfitHeight,否则JavaFX按视频原始尺寸渲染,录个1080P的MP4,MediaView默认就占满整个窗口。MediaPlayer是重量级组件,一次只能存在一个实例,切换播放文件前必须调用dispose()释放旧实例,否则第二次播放会一直黑屏。

6. 多线程排错与帧率自适应的编码技巧

6.1 录制中途切换分辨率或缩放比例怎么办

录屏时如果目标应用从全屏切到窗口模式,屏幕分辨率不变但捕获区域内容大变,Robot仍然能继续抓,但编码器的分辨率参数是在start()时定死的,不能中途改。我一般捕获区域固定为"虚拟屏幕的完整尺寸",然后把需要录制的窗口位置和大小换算到屏幕坐标系里,通过setCaptureRect动态调整。

但说实话,"动态改分辨率"在录制场景里很难处理干净——画面会拉伸或出现黑边。更稳妥的方案是启动录制前让用户固定选好区域,录制过程中不提供改分辨率的功能,只支持"预览区域"的缩放。这个约束在交互设计上就告诉用户:录屏的分辨率在开始时确定,中途改动不在能力范围内。

6.2 帧率掉不下来:动态降帧策略

录制高动态画面(比如拖动窗口、播放视频)时,scheduleAtFixedRate固定30 FPS可能让CPU和内存都吃紧。我一般会在帧采集后检查时间戳,如果前100帧的平均编码耗时超过50 ms,就意味着当前机器跑不满30 FPS,这时手动跳过部分帧。跳过帧也会导致PTS不连续,播放端表现为"快进感"。所以最优解是降低目标帧率而不是跳帧。

private void captureFrame() { long startNanos = System.nanoTime(); BufferedImage image = robot.createScreenCapture(screenRect); Frame frame = converter.convert(image); recorder.record(frame); long elapsed = (System.nanoTime() - startNanos) / 1_000_000; if (elapsed > frameIntervalMs) { int newFps = (int) (FPS * 0.9); // 降帧率 10% adjustFrameRate(newFps); } } private void adjustFrameRate(int newFps) { if (newFps >= minFps) { recorder.setFrameRate(newFps); FPS = newFps; } }

adjustFrameRate里调recorder.setFrameRate()是JavaCV支持的做法,它会在编码器内部调整时间基,不产生PTS中断。注意加一个下限保护(minFps),否则极端情况下帧率会一路降到个位数,编码器会抛异常。

6.3 时间换算:录制时长还是暂停时长

暂停功能如果做了,录制总时长的展示就有两个口径:自然时间(从开始到停止的墙钟时间)和有效时长(去掉暂停段)。JavaFX界面上显示"录制时长 02:13"时,用户期望的是有效时长——暂停的那段时间不该算进去。实现上维护一个pausedDuration累加器:

public void pause() { if (isRecording && !isPaused) { pauseStart = System.currentTimeMillis(); isPaused = true; // 暂停录制任务,但不停止 recorder } } public void resume() { if (isPaused) { pausedDuration += System.currentTimeMillis() - pauseStart; isPaused = false; pauseStart = 0; } } public long getEffectiveDuration() { return isRecording ? System.currentTimeMillis() - startTime - pausedDuration : 0; }

getEffectiveDuration同时考虑录制中状态,一旦停止就不再更新。这段逻辑与第3.3节中暂停的wait/notify配套,暂停点击时更新pausedDuration,恢复时重置pauseStart。时间精度到毫秒足够,别去用System.nanoTime(),转换成本高且没有意义。最终MP4的duration由编码器的PTS决定,所以暂停逻辑写了多久,文件的时间轴就是多久,与UI显示的有效时长完全对上。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/16 16:55:03

GPOPS-II伪谱法最优控制建模:从setup结构体到Bryson-Denham问题

简介:面向需要求解最优控制问题的MATLAB用户,这份资源提供完整的GPOPS工具箱及配套示例库,覆盖最小爬升、运载火箭上升、高灵敏边界值等多类经典问题,每个案例均包含带详细中文注释的脚本、可运行的Main入口、问题描述txt及求解输…

作者头像 李华
网站建设 2026/9/16 16:51:58

STM32+RFID宿舍门禁系统:从硬件到Android联调全解析

简介:基于STM32单片机与射频识别技术实现的宿舍门禁系统,配套完整的安卓端手机应用源码和毕业设计资料,适合嵌入式、物联网、软件工程等专业学生直接用于毕业设计、课程设计或项目初期演示。整个压缩包共包含五十六个文件,体积仅一…

作者头像 李华
网站建设 2026/9/16 16:50:45

微电网双层优化配置:混合储能系统容量规划方法

1. 项目概述:微电网容量配置的双层优化方法论在可再生能源占比不断提升的能源格局下,微电网作为分布式能源的重要载体,其规划配置的合理性直接影响系统经济性和可靠性。传统单层优化方法往往难以兼顾投资成本、运行约束与动态响应等多重目标&…

作者头像 李华
网站建设 2026/9/16 16:49:56

FPGA呼吸灯设计:基于Verilog的PWM控制与Vivado实现

简介:围绕Nexys4 DDR FPGA开发板实现的RGB呼吸灯控制项目,面向FPGA初学者、数字电路设计爱好者及电子类课程实验者。通过一个可观测的LED渐变项目,串起FPGA基本概念、硬件描述语言编程、GPIO引脚配置、时序逻辑设计、仿真验证、JTAG下载调试等…

作者头像 李华