如何用 Rust 实现的基础屏幕录制程序?

发布于:2025-09-15 ⋅ 阅读:(23) ⋅ 点赞:(0)

以下是一个使用 Rust 实现的基础屏幕录制程序示例,支持导出为 MP4,并提供绿色部署的说明。该示例基于跨平台屏幕捕获库和 FFmpeg 编码库,适用于 Windows/macOS/Linux(需根据系统调整依赖)。
在这里插入图片描述

一、项目准备

1. 创建项目
cargo new screen_recorder
cd screen_recorder
2. 添加依赖(Cargo.toml
[package]
name = "screen_recorder"
version = "0.1.0"
edition = "2021"

[dependencies]
# 屏幕捕获(跨平台)
screen-capture-rs = "0.3.0"
# 视频编码/封装(FFmpeg 绑定)
ffmpeg = { version = "0.5.0", features = ["codec", "format", "software-scaling"] }
# 图像格式转换
image = "0.24.7"
# 线程间通信
crossbeam-channel = "0.5.8"
# 锁优化
parking_lot = "0.12.1"
# 懒加载
lazy_static = "1.4.0"
# 系统事件监听(可选,用于退出)
miette = "5.10.0"  # 错误提示
thiserror = "1.0.40"  # 自定义错误

在这里插入图片描述

二、核心代码实现

以下是简化的录屏逻辑,包含屏幕捕获、RGB 转 YUV420p、H.264 编码、MP4 封装功能。

src/main.rs
use std::sync::Arc;
use std::time::{Duration, Instant};

use crossbeam_channel::{bounded, Receiver, Sender};
use ffmpeg::{
    codec, codec::encoder::Video, codec::id::CodeId, format, format::pixel::Pixel, media::Type,
    util::frame::video::Video as Frame,
};
use image::{ImageBuffer, Rgb};
use parking_lot::Mutex;
use screen_capture_rs::{ScreenCapturer, ScreenID};
use thiserror::Error;

#[derive(Error, Debug)]
enum Recorder_error {
    #[error("FFmpeg error: {0}")]
    Ffmpeg(#[from] ffmpeg::Error),
    #[error("Channel error")]
    Channel,
    #[error("No screen available")]
    NoScreen,
}

type Result<T> = std::result::Result<T, recorder_error>;

/// 屏幕录制器核心结构体
struct ScreenRecorder {
    capturer: ScreenCapturer,
    width: u32,
    height: u32,
    frame_sender: Sender<Arc<Mutex<Frame>>>,
}

impl ScreenRecorder {
    /// 初始化屏幕捕获
    fn new() -> Result<Self> {
        let capturer = ScreenCapturer::new()?;
        let screens = capturer.screens()?;
        let screen = screens.first().ok_or(recorder_error::NoScreen)?;
        let (width, height) = (screen.width, screen.height);

        // 创建通道用于传递帧到编码线程
        let (frame_sender, frame_receiver) = bounded(10);

        Ok(Self {
            capturer,
            width,
            height,
            frame_sender,
        })
    }

    /// 开始录制(阻塞直到完成)
    fn record(&mut self, output_path: &str, duration: Duration) -> Result<()> {
        // 启动编码线程
        let encoder = VideoEncoder::new(output_path, self.width, self.height)?;
        let receiver = Arc::new(Mutex::new(frame_receiver));

        // 启动编码线程
        let encoder_thread = std::thread::spawn(move || {
            encoder.encode(receiver);
        });

        // 主线程捕获屏幕帧
        let start_time = Instant::now();
        while start_time.elapsed() < duration {
            // 捕获一帧(阻塞直到有新帧)
            let frame = self.capturer.frame()?;
            let rgb_frame = ImageBuffer::<Rgb<u8>, Vec<u8>>::from(frame.rgb_pixel_data());

            // 转换为 YUV420p 格式(FFmpeg H.264 编码需要)
            let yuv_frame = Self::rgb_to_yuv420p(&rgb_frame, self.width, self.height)?;

            // 发送到编码线程
            if self.frame_sender.send(Arc::new(Mutex::new(yuv_frame))).is_err() {
                break;
            }
        }

        // 等待编码线程完成
        encoder_thread.join().map_err(|_| recorder_error::Channel)?;
        Ok(())
    }

    /// RGB 转 YUV420p(简化实现,实际需优化)
    fn rgb_to_yuv420p(rgb_frame: &ImageBuffer<Rgb<u8>, Vec<u8>>, width: u32, height: u32) -> Result<Frame> {
        let mut yuv_frame = Frame::new(width, height, Pixel::Yuv420p);
        yuv_frame.planes()[0].copy_from_slice(&rgb_frame.chunks_exact(3).flat_map(|p| [p[0]]).collect::<Vec<u8>>());
        // 注意:此处省略 U/V 平面的转换逻辑,实际需完整实现 RGB 到 YUV420p 的转换
        Ok(yuv_frame)
    }
}

/// H.264 编码器
struct VideoEncoder {
    output: format::output::Output,
    stream: format::stream::Stream,
    codec_ctx: codec::context::Context<Video>,
}

impl VideoEncoder {
    /// 初始化编码器
    fn new(output_path: &str, width: u32, height: u32) -> Result<Self> {
        ffmpeg::init()?;

        // 创建输出上下文
        let mut output = format::output::Output::create(output_path)?;
        output.set_flags(format::flag::Flags::AUTO_TS);

        // 查找 H.264 编码器
        let codec = codec::encoder::find_by_name("libx264").ok_or(ffmpeg_error::CodecNotFound)?;
        let mut codec_ctx = codec::context::Context::new(codec);
        codec_ctx.set_width(width);
        codec_ctx.set_height(height);
        codec_ctx.set_time_base(ffmpeg::Rational::new(1, 30)); // 30 FPS
        codec_ctx.set_pix_fmt(Pixel::Yuv420p);
        codec_ctx.set_bit_rate(4_000_000); // 4 Mbps
        codec_ctx.set_gop_size(10); // 关键帧间隔

        // 配置 H.264 参数(可选)
        let params = codec_ctx.parameters();
        let mut codec_ctx = codec::context::Context::from_parameters(params)?;
        codec_ctx.set_option("preset", "ultrafast"); // 快速编码(牺牲压缩率)
        codec_ctx.set_option("crf", "23"); // 质量系数(0-51,越小质量越好)

        // 打开编码器
        codec_ctx.open()?;

        // 添加视频流到输出
        let stream = output.add_stream(codec)?;
        stream.codec().set_parameters(codec_ctx.parameters())?;

        // 写入文件头
        output.write_header()?;

        Ok(Self { output, stream, codec_ctx })
    }

    /// 编码并写入视频
    fn encode(&mut self, receiver: Arc<Mutex<Receiver<Arc<Mutex<Frame>>>>>) {
        let mut frame_count = 0;
        let start_time = Instant::now();

        loop {
            // 接收帧(超时 1 秒)
            let frame = match receiver.lock().recv_timeout(Duration::from_secs(1)) {
                Ok(f) => f.lock(),
                Err(_) => break, // 发送端关闭
            };

            // 设置帧时间戳
            let pts = frame_count as i64;
            frame.set_pts(pts);
            frame_count += 1;

            // 发送帧到编码器
            self.codec_ctx.send_frame(&frame)?;

            // 接收编码后的包
            while let Some(packet) = self.codec_ctx.receive_packet()? {
                packet.set_stream_index(self.stream.index());
                self.output.interleaved_write_packet(&packet)?;
            }
        }

        // 刷新编码器剩余数据
        self.codec_ctx.flush()?;
        self.output.write_trailer()?;
    }
}

fn main() -> Result<()> {
    // 录制 10 秒屏幕(可修改)
    let mut recorder = ScreenRecorder::new()?;
    recorder.record("output.mp4", Duration::from_secs(10))?;
    println!("录制完成,输出文件:output.mp4");
    Ok(())
}

三、关键说明

1. 屏幕捕获
  • 使用 screen-capture-rs 库跨平台捕获屏幕(需系统权限:Windows 需允许屏幕访问,macOS 需开启“屏幕录制”权限,Linux 需 X11 权限)。
  • ScreenCapturer::frame() 会阻塞直到获取到新帧。
2. 视频编码
  • 使用 FFmpeg 的 libx264 编码器(需系统安装 FFmpeg 或静态链接)。
  • 注意:示例中 rgb_to_yuv420p 函数未完整实现 RGB 到 YUV420p 的转换(实际需按 BT.601/BT.709 标准转换),建议使用 sws_scale(FFmpeg 的缩放/格式转换函数)优化。
3. MP4 封装
  • FFmpeg 自动处理 MP4 容器封装,写入头/尾信息。

在这里插入图片描述

四、绿色部署(静态编译)

绿色部署要求生成单个可执行文件,不依赖外部动态库。以 Linux 和 Windows 为例:

1. Linux(静态编译)
# 安装 musl 工具链(Ubuntu/Debian)
sudo apt-get install musl-tools

# 配置 cargo 使用 musl 目标
rustup target add x86_64-unknown-linux-musl

# 编译(静态链接)
cargo build --target x86_64-unknown-linux-musl --release

# 输出文件:target/x86_64-unknown-linux-musl/release/screen_recorder
2. Windows(静态编译)
  • 使用 MSVC 工具链(默认)编译的 Windows 可执行文件通常依赖 vcruntime140.dll,可通过以下方式静态链接:
    1. Cargo.toml 中添加:
      [target.x86_64-pc-windows-msvc]
      rustflags = ["-C", "target-feature=+crt-static"]
      
    2. 编译:
      cargo build --target x86_64-pc-windows-msvc --release
      
    3. 输出文件:target/x86_64-pc-windows-msvc/release/screen_recorder.exe

五、注意事项

  1. 性能优化:示例中的 RGB 转 YUV 是简化实现,实际需使用 FFmpeg 的 sws_getCachedContext 进行高效转换。
  2. 音频支持:如需录制音频,可结合 cpal 库捕获音频,并与视频同步封装(需处理音视频时间戳对齐)。
  3. 错误处理:示例简化了错误处理,实际需完善边界条件(如屏幕分辨率变化、编码失败等)。
  4. FFmpeg 依赖:若系统未安装 FFmpeg,需静态链接 FFmpeg 库(如使用 https://github.com/joel16/ffmpeg-musl)。

六、扩展建议

  • GUI 界面:添加系统托盘图标或简单 GUI(如使用 eguirelm4)。
  • 快捷键控制:通过 global-hotkey 库实现全局快捷键(开始/暂停/停止)。
  • 实时预览:使用 minifbwinit 显示录制画面预览。
  • 多线程优化:分离屏幕采集、格式转换、编码为独立线程,提升性能。

完整实现需处理更多细节(如时间戳同步、编码参数调优),建议参考 https://ffmpeg.org/doxygen/trunk/ 和 https://github.com/zmwangx/rust-ffmpeg/tree/master/examples。