Java实现跨平台屏幕录制:从Robot捕获到FFmpeg编码的完整实践
1. 项目概述从需求到实现的思考路径最近在做一个内部工具时碰到了一个挺实际的需求需要把软件的操作过程自动录制成视频方便后续做演示或者排查问题。一开始想找现成的工具但要么功能太臃肿要么无法很好地集成到Java应用里进行程序化控制。于是干脆自己动手用Java来实现一个轻量级的屏幕录制功能。这听起来可能有点“重复造轮子”但自己实现的好处是可控性极高你可以精确地控制录制的区域、帧率、编码格式并且能无缝地嵌入到你的Java应用中实现自动化录制。这个需求背后其实涉及几个核心点首先是如何捕获屏幕画面也就是从操作系统获取当前显示器的像素数据其次是如何处理这些连续的图像帧将它们编码成主流的视频格式如MP4最后也是更具挑战性的是如何支持多显示器环境。现在很多开发者和用户都使用双屏甚至多屏办公录制单个屏幕已经不够用了我们需要能灵活选择录制哪个屏幕或者甚至录制一个跨越多个显示器的虚拟区域。实现这个功能你会接触到Java在桌面应用开发、图像处理以及多媒体封装方面的能力。它不只是一个简单的API调用更需要你理解屏幕捕获的原理、视频编码的基础知识以及不同操作系统Windows, macOS, Linux在图形接口上的差异。下面我就把自己从零搭建这个功能的过程、踩过的坑以及一些优化心得详细地分享出来。2. 核心技术选型与架构设计要实现屏幕录制我们需要一套清晰的“流水线”捕获 - 处理 - 编码 - 封装。每个环节都有不同的技术方案可选我的选型主要基于“轻量、高效、跨平台”的原则。2.1 屏幕捕获方案Robot vs. 原生APIJava标准库中最直接的屏幕捕获工具是java.awt.Robot类。它的createScreenCapture(Rectangle screenRect)方法可以获取指定矩形区域的屏幕截图返回一个BufferedImage对象。这个方案的最大优点是纯Java实现跨平台在Windows、macOS和Linux上都能运行。但是Robot类在高频率捕获时例如每秒30帧存在明显的性能瓶颈。它的捕获操作是同步的并且依赖于操作系统的底层事件机制在部分系统上可能会触发全局屏幕刷新导致CPU占用率偏高。对于需要长时间、高帧率录制的情况这可能会成为问题。另一种方案是使用各平台的原生API例如Windows的GDI或DirectXmacOS的CoreGraphicsLinux的X11或Wayland。通过JNIJava Native Interface调用这些API可以获得更高的捕获效率和更丰富的功能如捕获指定窗口、捕获硬件加速渲染的内容。著名的开源项目FFmpeg在实现屏幕录制时就大量使用了各平台的原生捕获模块。我的选择与理由对于大多数Java应用场景尤其是希望保持部署简单、避免引入复杂原生依赖的情况Robot类仍然是首选。它的性能对于制作教程、记录操作日志通常15-30帧已经足够。在本项目中我们以Robot为基础进行实现同时会探讨一些优化其性能的技巧。多屏支持的核心在于正确获取所有屏幕的布局信息而Robot同样可以做到这一点。2.2 视频编码与封装FFmpeg的桥梁作用获取到连续的BufferedImage帧后我们需要将它们压缩并打包成视频文件。Java生态中并没有强大的内置视频编码库因此引入外部工具是必然的。这里FFmpeg几乎是唯一且最佳的选择。它是一个完整的、跨平台的音视频处理解决方案支持几乎所有已知的编码格式。我们不需要在Java中重新实现编码器而是通过进程调用的方式将图像帧数据通过管道Process的OutputStream实时传递给FFmpeg进程由FFmpeg完成高效的编码和文件写入。这种方式将复杂的编码工作交给了久经考验的专业工具我们只需专注于图像的捕获和传递。编码格式选择H.264 / AVC最通用的选择兼容性极好压缩率高。使用libx264编码器。HEVC / H.265更高的压缩效率适合对文件大小有严格要求的场景但编解码开销稍大部分旧设备可能不支持。VP9开放、免专利费的格式Web端支持好但编码速度较慢。对于屏幕录制内容大量静态区域、文本推荐使用H.264编码并配合tunezerolatency零延迟和presetultrafast超快参数以优先保证编码速度和实时性画质损失在可接受范围内。2.3 整体架构设计基于以上选型我们的系统架构可以设计如下配置模块用户指定录制区域全屏、单个屏幕、自定义矩形、帧率、输出文件路径、视频参数分辨率、码率、编码器。捕获调度器一个独立的线程按照设定的帧率循环工作。在每次循环中它调用Robot.captureScreen获取当前帧的BufferedImage。图像处理管道可选对捕获的原始图像进行预处理例如缩放至目标分辨率、转换为FFmpeg接受的像素格式如BGRA或RGB24。FFmpeg进程管理器启动一个FFmpeg子进程配置好输入从标准输入stdin读取原始视频帧和输出写入MP4文件。Java端将处理后的图像帧数据写入该进程的输出流。控制模块提供开始、暂停、停止录制的API并负责资源的清理停止捕获线程、关闭FFmpeg进程。对于多屏录制关键在于第一步的配置模块。我们需要通过GraphicsEnvironment获取所有GraphicsDevice屏幕设备并计算出每个屏幕的绝对坐标位置从而允许用户指定一个可能跨越多个屏幕的录制区域。3. 核心实现细节与代码拆解接下来我们深入到代码层面看看每个核心环节如何实现。我会先给出单屏录制的基础版本再扩展到多屏。3.1 获取屏幕信息与定义录制区域这是所有工作的起点。我们需要知道屏幕有多大有多少块屏幕它们是怎么排列的。import java.awt.*; import java.util.ArrayList; import java.util.List; public class ScreenRecorder { private Robot robot; private Rectangle captureArea; // 要录制的矩形区域 public ScreenRecorder() throws AWTException { this.robot new Robot(); } /** * 获取系统中所有屏幕的边界信息。 * return 包含所有屏幕Rectangle的列表 */ public static ListRectangle getAllScreenBounds() { GraphicsEnvironment ge GraphicsEnvironment.getLocalGraphicsEnvironment(); GraphicsDevice[] screens ge.getScreenDevices(); ListRectangle bounds new ArrayList(); for (GraphicsDevice screen : screens) { bounds.add(screen.getDefaultConfiguration().getBounds()); } return bounds; } /** * 计算所有屏幕构成的虚拟全屏边界。 * 例如两个1920x1080的屏幕左右并列则返回一个3840x1080的Rectangle。 * return 虚拟全屏的Rectangle */ public static Rectangle getVirtualScreenBounds() { ListRectangle screenBounds getAllScreenBounds(); if (screenBounds.isEmpty()) { return new Rectangle(0, 0, 0, 0); } // 计算能包围所有屏幕的最小矩形 int minX Integer.MAX_VALUE; int minY Integer.MAX_VALUE; int maxX Integer.MIN_VALUE; int maxY Integer.MIN_VALUE; for (Rectangle rect : screenBounds) { minX Math.min(minX, rect.x); minY Math.min(minY, rect.y); maxX Math.max(maxX, rect.x rect.width); maxY Math.max(maxY, rect.y rect.height); } return new Rectangle(minX, minY, maxX - minX, maxY - minY); } /** * 设置录制区域。 * param area 要录制的矩形区域基于虚拟坐标系 */ public void setCaptureArea(Rectangle area) { this.captureArea area; // 简单验证区域是否在虚拟屏幕范围内 Rectangle virtualBounds getVirtualScreenBounds(); if (!virtualBounds.contains(area)) { System.err.println(警告录制区域部分或全部在屏幕可见范围之外。); } } }关键点解析GraphicsDevice代表一个屏幕设备。它的getBounds()返回的Rectangle中的x和y坐标是全局虚拟坐标系下的位置。主屏通常是(0, 0)副屏可能在(1920, 0)右扩展或(-1920, 0)左扩展。getVirtualScreenBounds()函数计算出的矩形就是你能“录制”的最大区域。如果你想录制所有屏幕的内容直接将这个矩形设为捕获区域即可。坐标原点在Windows和大多数Linux桌面环境中虚拟坐标系的原点(0,0)位于主显示器的左上角。macOS的行为可能略有不同但Robot类会处理好这些差异。3.2 启动FFmpeg进程与配置编码参数我们需要在Java中构造正确的FFmpeg命令并启动进程。核心是让FFmpeg从stdin读取原始视频帧。import java.io.*; import java.util.concurrent.atomic.AtomicBoolean; public class FFmpegRecorder { private Process ffmpegProcess; private OutputStream ffmpegInput; private AtomicBoolean isRecording new AtomicBoolean(false); private Thread captureThread; /** * 启动FFmpeg进程准备接收原始帧数据。 * param outputFile 输出文件路径如 output.mp4 * param frameRate 帧率如 30 * param width 视频宽度 * param height 视频高度 * throws IOException 如果启动进程失败 */ public void start(String outputFile, int frameRate, int width, int height) throws IOException { // 构造FFmpeg命令 // -y: 覆盖输出文件 // -f rawvideo: 输入格式为原始视频 // -pixel_format bgr0: 输入像素格式与BufferedImage.TYPE_INT_ARGB转换后的数据对应 // -video_size wxh: 输入视频尺寸 // -framerate fr: 输入帧率 // -i -: 从标准输入读取 // -c:v libx264: 使用H.264编码 // -preset ultrafast: 编码预设追求速度 // -tune zerolatency: 零延迟调优适合实时流 // -pix_fmt yuv420p: 输出像素格式兼容性最好 // -f mp4: 输出格式为MP4 String[] cmd { ffmpeg, -y, -f, rawvideo, -pixel_format, bgr0, // 注意Java的ARGB内存布局是A|R|G|B但FFmpeg的bgr0是B|G|R|A顺序不同需要转换。 -video_size, width x height, -framerate, String.valueOf(frameRate), -i, -, -c:v, libx264, -preset, ultrafast, -tune, zerolatency, -pix_fmt, yuv420p, -f, mp4, outputFile }; ProcessBuilder pb new ProcessBuilder(cmd); // 将错误流重定向到标准输出方便查看FFmpeg日志 pb.redirectErrorStream(true); ffmpegProcess pb.start(); // 获取向FFmpeg写入数据的流 ffmpegInput ffmpegProcess.getOutputStream(); isRecording.set(true); // 启动一个线程来读取FFmpeg的输出日志防止进程阻塞 new Thread(() - { try (BufferedReader reader new BufferedReader(new InputStreamReader(ffmpegProcess.getInputStream()))) { String line; while ((line reader.readLine()) ! null) { System.out.println([FFmpeg] line); } } catch (IOException e) { if (isRecording.get()) { e.printStackTrace(); } } }).start(); } }重要提示像素格式的坑。BufferedImage内部存储的像素格式TYPE_INT_ARGB是AARRGGBB按字节顺序。而FFmpeg的bgr0格式期望的是BB GG RR AA。直接写入会导致颜色完全错误。我们必须在写入前进行转换。一个更稳妥、兼容性更好的方式是使用rgb24格式并忽略Alpha通道将ARGB转换为RGB。3.3 屏幕捕获循环与帧数据传递这是最核心的循环它需要稳定、高效地运行。public class FFmpegRecorder { // ... 接上文代码 /** * 开始屏幕捕获并编码。 * param captureArea 捕获区域 * param frameRate 目标帧率 */ public void startCapture(Rectangle captureArea, int frameRate) { if (captureThread ! null captureThread.isAlive()) { System.err.println(捕获线程已在运行。); return; } captureThread new Thread(() - { Robot robot; try { robot new Robot(); } catch (AWTException e) { e.printStackTrace(); return; } int width captureArea.width; int height captureArea.height; long frameIntervalMs 1000 / frameRate; // 每帧的理论间隔毫秒 BufferedImage capturedImage; byte[] frameBuffer; // 预分配缓冲区避免在循环中频繁创建对象 // 计算RGB24格式一帧的大小width * height * 3 (R, G, B各一个字节) int frameSize width * height * 3; frameBuffer new byte[frameSize]; System.out.println(开始屏幕捕获区域: captureArea , 帧率: frameRate FPS); while (isRecording.get()) { long frameStartTime System.currentTimeMillis(); // 1. 捕获屏幕 capturedImage robot.createScreenCapture(captureArea); // 2. 将BufferedImage转换为RGB24字节数组 convertImageToRGB24(capturedImage, frameBuffer); try { // 3. 将帧数据写入FFmpeg进程 ffmpegInput.write(frameBuffer); ffmpegInput.flush(); // 确保数据被推送 } catch (IOException e) { // 如果写入失败例如FFmpeg进程异常退出停止录制 System.err.println(写入FFmpeg失败: e.getMessage()); stop(); break; } // 4. 帧率控制计算本次捕获编码耗时进行休眠以稳定帧率 long processingTime System.currentTimeMillis() - frameStartTime; long sleepTime frameIntervalMs - processingTime; if (sleepTime 0) { try { Thread.sleep(sleepTime); } catch (InterruptedException e) { Thread.currentThread().interrupt(); break; } } else { // 如果处理时间超过帧间隔说明当前负载下无法达到目标帧率 // 可以记录一个警告或者动态调整捕获区域/质量 // System.err.println(警告帧处理超时当前负载较高。); } } System.out.println(屏幕捕获线程结束。); }); captureThread.start(); } /** * 将BufferedImage (TYPE_INT_ARGB) 转换为RGB24格式的字节数组。 * 忽略Alpha通道。 * param image 源图像 * param buffer 目标缓冲区大小必须为 width*height*3 */ private void convertImageToRGB24(BufferedImage image, byte[] buffer) { int width image.getWidth(); int height image.getHeight(); int[] pixels new int[width * height]; // 一次性获取所有像素的ARGB值效率比逐个getRGB高 image.getRGB(0, 0, width, height, pixels, 0, width); int pixelIndex 0; int bufferIndex 0; for (int y 0; y height; y) { for (int x 0; x width; x) { int argb pixels[pixelIndex]; // 提取R, G, B分量并放入buffer buffer[bufferIndex] (byte) ((argb 16) 0xFF); // R buffer[bufferIndex] (byte) ((argb 8) 0xFF); // G buffer[bufferIndex] (byte) (argb 0xFF); // B // 忽略Alpha: (argb 24) 0xFF } } } public void stop() { isRecording.set(false); if (captureThread ! null) { try { captureThread.join(1000); // 等待捕获线程结束 } catch (InterruptedException e) { e.printStackTrace(); } } if (ffmpegInput ! null) { try { ffmpegInput.close(); // 关闭输入流告知FFmpeg输入结束 } catch (IOException e) { e.printStackTrace(); } } if (ffmpegProcess ! null) { try { // 等待FFmpeg进程完成最后的写入和封装 int exitCode ffmpegProcess.waitFor(); System.out.println(FFmpeg进程退出代码: exitCode); } catch (InterruptedException e) { e.printStackTrace(); ffmpegProcess.destroyForcibly(); } } } }性能优化关键点预分配缓冲区在循环外创建frameBuffer和pixels数组避免在每秒数十次的循环中频繁创建和垃圾回收这对性能影响巨大。批量获取像素使用BufferedImage.getRGB(int, int, int, int, int[], int, int)一次性获取所有像素远比在嵌套循环中调用image.getRGB(x, y)高效。帧率控制简单的Thread.sleep控制并不精确但足以应对屏幕录制场景。更精确的控制可以使用ScheduledExecutorService。如果处理一帧的时间超过了设定的帧间隔说明系统负载过高可以考虑动态降低捕获分辨率或帧率。颜色空间转换这里我们选择了兼容性最好的RGB24格式。转换过程是纯CPU计算是主要的性能开销之一。如果对性能有极致要求可以研究使用JavaCVOpenCV的Java封装或JNI调用更优化的原生转换函数。3.4 多屏录制的使用示例将上面的模块组合起来实现多屏录制就非常直观了。public class MultiScreenRecordingDemo { public static void main(String[] args) throws AWTException, IOException, InterruptedException { // 1. 获取所有屏幕信息 ListRectangle screens ScreenRecorder.getAllScreenBounds(); System.out.println(检测到 screens.size() 个屏幕:); for (int i 0; i screens.size(); i) { Rectangle rect screens.get(i); System.out.printf( 屏幕 %d: 位置(%d, %d), 分辨率 %dx%d%n, i, rect.x, rect.y, rect.width, rect.height); } // 2. 创建录制器 FFmpegRecorder recorder new FFmpegRecorder(); // 3. 选择录制区域 Rectangle areaToRecord; if (screens.size() 1) { // 单屏录制整个主屏 areaToRecord screens.get(0); System.out.println(录制模式单屏全屏); } else { // 多屏示例1录制所有屏幕组成的虚拟全屏 // areaToRecord ScreenRecorder.getVirtualScreenBounds(); // System.out.println(录制模式虚拟全屏所有屏幕); // 多屏示例2仅录制第二个屏幕索引1 int screenIndex 1; // 假设录制副屏 if (screenIndex screens.size()) { areaToRecord screens.get(screenIndex); System.out.println(录制模式仅录制屏幕 screenIndex); } else { System.out.println(指定的屏幕索引无效回退到主屏。); areaToRecord screens.get(0); } // 多屏示例3录制一个自定义区域例如跨越主屏和副屏的一部分 // areaToRecord new Rectangle(500, 200, 2500, 800); // 自定义坐标和大小 // System.out.println(录制模式自定义区域); } // 4. 配置并启动FFmpeg int frameRate 20; String outputFile screen_record_ System.currentTimeMillis() .mp4; recorder.start(outputFile, frameRate, areaToRecord.width, areaToRecord.height); // 5. 开始捕获 recorder.startCapture(areaToRecord, frameRate); System.out.println(录制已开始输出至: outputFile 按Enter键停止...); System.in.read(); // 等待用户按下回车 // 6. 停止录制 recorder.stop(); System.out.println(录制已停止。); } }4. 常见问题、优化与深度排查在实际使用中你肯定会遇到各种各样的问题。下面是我在开发和测试过程中总结的一些典型情况及其解决方法。4.1 性能问题与优化策略问题表现录制时CPU占用率过高例如超过50%或者实际帧率远低于设定帧率。排查与解决降低捕获分辨率这是最有效的优化手段。全屏如3840x2160捕获的数据量是1080p的4倍。如果不需要超清可以将捕获区域缩小或者在捕获后使用BufferedImage.getScaledInstance()进行缩放再将缩小后的图像传递给编码器。注意缩放操作本身也有开销需要在捕获线程外或使用更高效的图像缩放库如Thumbnailator进行。调整FFmpeg编码预设我们使用了-preset ultrafast这已经是为了速度牺牲了压缩率。如果CPU依然是瓶颈可以尝试降低帧率如从30降到15或10或者使用更简单的编码器如mpeg4但文件会变大。审视颜色转换convertImageToRGB24函数中的循环是CPU密集型的。可以尝试以下优化使用DataBuffer直接操作对于已知的BufferedImage类型如TYPE_INT_RGB可以直接访问其底层的DataBufferInt避免getRGB的调用开销。并行处理对于大图像可以将行拆分用多线程并行进行颜色转换。但要注意线程创建和同步的开销可能抵消收益。使用JNI调用原生代码终极优化方案将转换逻辑用C/C实现并通过JNI调用。这对于追求极致性能的场景是值得的。一个简单的分辨率缩放示例private BufferedImage scaleImage(BufferedImage original, int targetWidth, int targetHeight) { // 使用平滑缩放模式质量较好但速度稍慢 Image scaled original.getScaledInstance(targetWidth, targetHeight, Image.SCALE_SMOOTH); BufferedImage output new BufferedImage(targetWidth, targetHeight, BufferedImage.TYPE_INT_RGB); Graphics2D g2d output.createGraphics(); g2d.drawImage(scaled, 0, 0, null); g2d.dispose(); return output; } // 在捕获循环中调用 BufferedImage captured robot.createScreenCapture(captureArea); BufferedImage scaled scaleImage(captured, targetWidth, targetHeight); // 然后将scaled图像转换为RGB244.2 视频质量问题问题表现录制的视频模糊、有拖影、颜色失真或文件异常大。排查与解决模糊与拖影这通常是因为帧率不足或编码延迟。确保你的帧率如20-30对于屏幕操作是足够的。检查-tune zerolatency参数是否已设置它减少了编码缓冲降低了延迟。拖影也可能是由于Robot捕获瞬间屏幕正在刷新可以尝试在捕获前加入极短的延迟如Thread.sleep(1)但这会进一步降低帧率。颜色失真99%的问题出在像素格式转换上。务必确认Java端BufferedImage的类型通常是TYPE_INT_ARGB。FFmpeg命令中-pixel_format参数设置是否正确我们用了rgb24。转换函数convertImageToRGB24是否正确地将ARGB的字节顺序转换成了RGB。可以录制一个纯色红、绿、蓝屏幕进行测试用播放器检查颜色。文件过大屏幕录制内容尤其是办公、代码界面包含大量静态区域使用H.264默认参数可能压缩率不够。可以尝试增加-crf参数恒定质量因子范围是0-51值越大压缩率越高、质量越低。对于屏幕录制-crf 23到-crf 28是不错的选择。-crf 28的文件会比默认小很多画质仍可接受。使用-preset slower可以获得更好的压缩率但编码速度会慢不适合实时录制。可以在后期处理非实时录制的文件时使用。4.3 FFmpeg相关错误排查FFmpeg进程可能因各种原因失败其错误信息会输出到标准错误流。我们已经通过redirectErrorStream(true)将其合并到了标准输出流中方便查看。常见错误及解决错误信息或现象可能原因解决方案Invalid argument/Invalid data found when processing inputFFmpeg命令参数错误特别是-pixel_format、-video_size与实际输入数据不匹配。仔细检查命令构造确保宽度、高度、像素格式字符串完全正确。打印出最终的cmd数组进行核对。pipe:: Input/output errorJava端在FFmpeg进程还未准备好或已结束时尝试写入数据。确保在startCapture之前成功调用了start()启动了FFmpeg进程。在stop()时先停止捕获线程再关闭输出流。进程启动立即退出无错误系统未安装FFmpeg或FFmpeg不在PATH环境变量中。在终端测试ffmpeg -version命令。在Java中可以使用绝对路径指定FFmpeg可执行文件位置如/usr/local/bin/ffmpeg或C:\\ffmpeg\\bin\\ffmpeg.exe。录制文件无法播放或只有音频FFmpeg编码过程被异常中断文件未正确封装。确保在停止录制时调用了ffmpegInput.close()和ffmpegProcess.waitFor()让FFmpeg完成写入文件尾。[libx264 ...] height not divisible by 2某些编码器如x264要求视频的宽度和高度必须是偶数。在设置捕获区域或缩放时确保最终传递给FFmpeg的宽和高是偶数。可以在代码中强制调整width (width / 2) * 2;调试技巧在开发阶段可以在FFmpeg命令中加入-loglevel debug参数获取更详细的日志。或者可以先输出到无损格式如-f nut -i - -c:v libx264 -preset ultrafast -f nut -输出到stdout虽然不生成文件但可以测试管道通信是否正常。4.4 内存与资源管理问题表现长时间录制后内存占用持续增长或出现OutOfMemoryError。排查与解决对象池在捕获循环中我们预分配了frameBuffer和pixels数组这很好。但robot.createScreenCapture()每次都会返回一个新的BufferedImage对象。对于长时间运行可以考虑重用有限的几个BufferedImage对象如果Robot类允许的话但通常它返回新对象。更实际的做法是确保这些临时对象在每次循环后能被快速垃圾回收。如果帧率很高可以适当调低。流关闭务必在stop()方法中正确关闭ffmpegInput和等待FFmpeg进程终止。否则会导致进程句柄和流资源泄漏。线程中断确保在程序退出时捕获线程能被正确中断 (isRecording.set(false)并thread.interrupt())防止线程僵尸。5. 进阶扩展与实用技巧基础功能实现后可以考虑以下几个方向来增强其实用性。5.1 录制系统音频与麦克风单纯的屏幕录像没有声音体验是不完整的。通过FFmpeg我们可以相对容易地加入音频。思路启动另一个FFmpeg进程或同一个进程的另一个输入流来捕获系统音频输出扬声器和麦克风输入然后将音视频流混合。Windows可以使用dshowDirectShow或wasapi设备捕获音频。查找音频设备ffmpeg -list_devices true -f dshow -i dummy捕获系统声音和麦克风可能需要虚拟音频驱动如VB-Audio Virtual Cable将系统音频输出重定向为输入设备。macOS使用avfoundation。查找设备ffmpeg -f avfoundation -list_devices true -i 捕获-f avfoundation -i :00通常代表屏幕系统音频Linux使用pulsePulseAudio或alsaALSA。复杂性音频采样率、声道数与视频的同步音画同步是一个复杂问题。对于入门项目一个更简单的方案是后期合成先录制无声视频再使用ffmpeg命令将视频文件与另一个单独录制的音频文件合并。这避免了实时同步的麻烦。5.2 增加鼠标高亮与点击效果为了让教程更清晰可以在捕获的帧上实时绘制鼠标位置和点击效果。private BufferedImage addMouseCursorToImage(BufferedImage original, Point mousePos) { BufferedImage imageWithCursor new BufferedImage( original.getWidth(), original.getHeight(), BufferedImage.TYPE_INT_ARGB); Graphics2D g2d imageWithCursor.createGraphics(); // 1. 绘制原始屏幕内容 g2d.drawImage(original, 0, 0, null); // 2. 绘制一个红色的圆圈表示鼠标位置 g2d.setColor(Color.RED); g2d.setStroke(new BasicStroke(2)); int cursorRadius 5; // 注意mousePos是全局坐标需要转换到当前捕获图像的相对坐标 int relativeX mousePos.x - captureArea.x; int relativeY mousePos.y - captureArea.y; if (captureArea.contains(mousePos)) { g2d.drawOval(relativeX - cursorRadius, relativeY - cursorRadius, 2 * cursorRadius, 2 * cursorRadius); // 可以在这里根据鼠标按键状态绘制点击动画 } g2d.dispose(); return imageWithCursor; } // 在捕获循环中获取鼠标位置 Point mousePos MouseInfo.getPointerInfo().getLocation(); BufferedImage captured robot.createScreenCapture(captureArea); BufferedImage finalImage addMouseCursorToImage(captured, mousePos); // 对finalImage进行编码5.3 图形化界面GUI封装对于最终用户一个图形界面是必不可少的。你可以使用Swing或JavaFX开发一个简单的控制面板包含屏幕选择列出所有屏幕的缩略图让用户点击选择录制哪个屏幕或区域。区域选择提供拖拽框选录制区域的功能。参数设置帧率、输出路径、视频质量CRF的下拉菜单或输入框。控制按钮开始、暂停、停止按钮以及录制时长、文件大小的显示。将我们上面实现的核心ScreenRecorder和FFmpegRecorder类作为后台引擎GUI前端通过事件监听器调用它们的方法即可。注意长时间的捕获和编码操作一定要放在后台线程如SwingWorker中执行避免阻塞GUI事件调度线程导致界面卡死。实现这个Java屏幕录制工具的过程是一次对Java桌面能力、多媒体处理和跨平台开发的深入实践。从最基础的Robot截图到与FFmpeg的进程通信再到多屏坐标系的处理每一步都需要仔细考量性能和兼容性。虽然它可能比不上OBS Studio这样的专业软件功能全面但其轻量、可集成、可编程的特性使其成为构建自动化测试、远程协助、操作审计等企业级应用内部工具的绝佳基础组件。最重要的是通过亲手实现你真正掌握了屏幕录制背后的原理下次再遇到相关问题你就能从容地从原理层面去分析和解决了。

相关新闻

Power BI批量导入多Sheet Excel:自动化数据整合与清洗实战

Power BI批量导入多Sheet Excel:自动化数据整合与清洗实战

1. 项目概述:为什么批量导入Excel是数据分析的“刚需”?如果你经常和数据打交道,尤其是从业务部门、财务系统或者各种渠道收集来的Excel报表,那你一定对下面这个场景不陌生:每个月末,邮箱里塞满了十几个甚至…

2026/8/7 4:05:20 阅读更多 →
Turbo Intruder进阶:从并发到精控的Web安全测试实战

Turbo Intruder进阶:从并发到精控的Web安全测试实战

1. 项目概述:从“并发”到“精控”的Turbo Intruder进阶之路如果你在安全测试或渗透测试领域摸爬滚打过一段时间,尤其是对Web应用进行漏洞挖掘时,Burp Suite的Turbo Intruder插件大概率已经是你工具箱里的常客。它以其强大的并发请求能力和灵…

2026/8/7 4:05:20 阅读更多 →
AI评论家Prompt设计:结构化提示词构建高效文本审阅框架

AI评论家Prompt设计:结构化提示词构建高效文本审阅框架

1. 项目概述:AI评论家Prompt的诞生与价值在学术研究和内容创作的日常里,我们常常会陷入一种困境:自己写的论文或文章,怎么看都觉得逻辑严密、文笔流畅,但交给导师、同行或读者一看,总能发现一堆自己未曾察觉…

2026/8/7 4:05:20 阅读更多 →

最新新闻

Python数据分析实战:泰坦尼克号生存预测的探索性数据分析全流程

Python数据分析实战:泰坦尼克号生存预测的探索性数据分析全流程

1. 项目概述:从泰坦尼克号到数据洞察“你能在泰坦尼克号上活下来吗?”——这不仅仅是一个引人入胜的历史假设,更是数据科学领域最经典的入门项目。它像一块试金石,让无数初学者通过Python和探索性数据分析(EDA&#xf…

2026/8/7 4:57:50 阅读更多 →
Keil MDK编译错误:cmsis_version.h找不到的完整解决方案

Keil MDK编译错误:cmsis_version.h找不到的完整解决方案

1. 问题现象与核心诊断当你满怀期待地在Keil MDK中点击编译按钮,准备迎接一个干净的“0 Error(s), 0 Warning(s)”时,屏幕上却弹出了一个令人沮丧的红色错误提示:error: #5: cannot open source input file “cmsis_version.h“: No such fil…

2026/8/7 4:57:50 阅读更多 →
Linux下载方案:Docker部署迅雷远程服务与Aria2配置指南

Linux下载方案:Docker部署迅雷远程服务与Aria2配置指南

1. 项目概述:在Linux上找回熟悉的下载体验 作为一个在Linux桌面环境里摸爬滚打了十多年的老用户,我太理解那种面对一个磁力链接或ed2k资源时的无力感了。Windows世界里,迅雷几乎是下载的代名词,其强大的P2P加速和资源整合能力&am…

2026/8/7 4:57:50 阅读更多 →
Codex跨界视频剪辑实测:自然语言驱动AI自动化工作流

Codex跨界视频剪辑实测:自然语言驱动AI自动化工作流

1. 项目概述:当Codex遇上视频剪辑,一次意料之外的跨界实测最近在AI圈子里,一个消息让我这个老码农也坐不住了:Codex,那个我们熟悉得不能再熟悉的代码生成模型,居然开始“跨界”玩起了视频剪辑?起…

2026/8/7 4:57:50 阅读更多 →
沧州建设局网站查询项目进度与办事指南全流程深度解析与避坑指南

沧州建设局网站查询项目进度与办事指南全流程深度解析与避坑指南

在当下的数字政务时代,无论是想要落户沧州的房地产开发商,还是辛苦打拼、期盼早日拿到不动产权证的家庭,亦或是关注城市基础设施建设的普通市民,我们都有一个共同的痛点,那就是信息获取的不透明和流程繁琐。提到沧州,很多人首先想到的是铁狮子,是武术之乡,是渤海之滨的…

2026/8/7 4:57:50 阅读更多 →
AI Agent技能开发实战:基于MCP协议构建可复用智能体能力

AI Agent技能开发实战:基于MCP协议构建可复用智能体能力

1. 从“经验”到“能力”:为什么我们需要Agent Skills?最近在折腾各种AI Agent框架时,我总在思考一个问题:我们花大量时间调教一个Agent,让它学会处理某个特定任务,比如分析日志、生成SQL或者格式化代码。这…

2026/8/7 4:56:49 阅读更多 →

日新闻

为什么scrcpy成为Android投屏的终极解决方案:完整实战指南

为什么scrcpy成为Android投屏的终极解决方案:完整实战指南

为什么scrcpy成为Android投屏的终极解决方案:完整实战指南 【免费下载链接】scrcpy Display and control your Android device 项目地址: https://gitcode.com/GitHub_Trending/sc/scrcpy 想要将Android手机屏幕完美投射到电脑上,享受大屏操作的自…

2026/8/7 0:00:19 阅读更多 →
如何在5分钟内掌握Tom Select:打造现代化表单选择器的终极指南

如何在5分钟内掌握Tom Select:打造现代化表单选择器的终极指南

如何在5分钟内掌握Tom Select:打造现代化表单选择器的终极指南 【免费下载链接】tom-select Tom Select is a lightweight (~16kb gzipped) hybrid of a textbox and select box. Forked from selectize.js to provide a framework agnostic autocomplete widget wi…

2026/8/7 0:00:19 阅读更多 →
5分钟快速上手:NSZ压缩工具终极指南,轻松管理Switch游戏文件

5分钟快速上手:NSZ压缩工具终极指南,轻松管理Switch游戏文件

5分钟快速上手:NSZ压缩工具终极指南,轻松管理Switch游戏文件 【免费下载链接】nsz NSZ - Homebrew compatible NSP/XCI compressor/decompressor 项目地址: https://gitcode.com/gh_mirrors/ns/nsz 你是否在为Nintendo Switch游戏文件占用大量存储…

2026/8/7 0:00:19 阅读更多 →

周新闻

最大流算法详解:从水管网络到Ford-Fulkerson与Dinic实战

最大流算法详解:从水管网络到Ford-Fulkerson与Dinic实战

1. 从水管网络到最大流:一个核心问题的诞生想象一下,你是一个城市供水系统的总工程师。你的城市有多个水源(水库),需要通过一个复杂的地下管道网络,将水输送到各个居民区。每条管道都有其最大通水能力&…

2026/8/6 22:02:27 阅读更多 →
基于Springboot的企业门户网站(源码+LW+调试文档+讲解)

基于Springboot的企业门户网站(源码+LW+调试文档+讲解)

温馨提示:本人主页置顶文章(点我)开头有 CSDN 平台官方提供的学长联系方式的名片! 温馨提示:本人主页置顶文章(点我)开头有 CSDN 平台官方提供的学长联系方式的名片! 温馨提示:本人主页置顶文章(点我)开头有 CSDN 平台…

2026/8/6 22:02:27 阅读更多 →
MATLAB xcorr函数详解:从互相关原理到四大实战应用

MATLAB xcorr函数详解:从互相关原理到四大实战应用

1. 从一次信号“找茬”说起:为什么我们需要互相关几年前,我在处理一组声学传感器数据时遇到了一个棘手的问题。我有两个麦克风记录了一段相同的音频信号,理论上它们接收到的声音波形应该非常相似,只是由于麦克风位置不同&#xff…

2026/8/6 22:02:27 阅读更多 →

月新闻

免费解锁百度网盘SVIP加速:macOS用户必备的下载提速终极指南

免费解锁百度网盘SVIP加速:macOS用户必备的下载提速终极指南

免费解锁百度网盘SVIP加速:macOS用户必备的下载提速终极指南 【免费下载链接】BaiduNetdiskPlugin-macOS For macOS.百度网盘 破解SVIP、下载速度限制~ 项目地址: https://gitcode.com/gh_mirrors/ba/BaiduNetdiskPlugin-macOS 还在为百度网盘macOS版的龟速下…

2026/8/5 23:28:39 阅读更多 →
终极ncmdump指南:3分钟实现网易云NCM音乐解密与格式转换

终极ncmdump指南:3分钟实现网易云NCM音乐解密与格式转换

终极ncmdump指南:3分钟实现网易云NCM音乐解密与格式转换 【免费下载链接】ncmdump 项目地址: https://gitcode.com/gh_mirrors/ncmd/ncmdump 还在为网易云音乐下载的NCM格式文件无法在其他播放器播放而烦恼吗?ncmdump解密工具帮你轻松解决这个困…

2026/8/6 22:02:28 阅读更多 →
HarmonyOS 应用开发《掌上英语》第81篇: 智能体卡片:为英语学习 App 打造桌面级学习助手

HarmonyOS 应用开发《掌上英语》第81篇: 智能体卡片:为英语学习 App 打造桌面级学习助手

AgentCard 智能体卡片:为英语学习 App 打造桌面级学习助手适用平台:HarmonyOS 7.0 (API 26 Beta)一、引言 HarmonyOS 7.0(API 26 Beta)新增了 AgentCard 智能体卡片能力,这是继 HMAF(鸿蒙智能体框架&#x…

2026/8/5 23:46:51 阅读更多 →