Rust 调用 FFmpeg 进行视频/音频处理 – wiki基地


深入探索:使用 Rust 驱动 FFmpeg 进行高性能视频与音频处理

在当今数字媒体驱动的世界中,视频和音频处理已成为众多应用程序的核心功能,从简单的格式转换到复杂的实时流处理、特效渲染和内容分析。FFmpeg 作为开源多媒体处理领域的瑞士军刀,以其无与伦比的格式兼容性、强大的编解码能力和丰富的滤镜系统,成为了事实上的行业标准。而 Rust,作为一门以内存安全、并发性和高性能著称的现代系统编程语言,正迅速在需要可靠性和效率的领域崭露头角。

将 Rust 的安全、高效与 FFmpeg 的强大功能相结合,无疑为构建下一代多媒体应用程序开辟了新的可能性。本文将深入探讨如何在 Rust 项目中调用 FFmpeg,分析两种主要方法的优劣,并提供实践指导和代码示例,帮助开发者驾驭这一强大的组合。

文章字数: 约 3500 字

1. 背景:为何选择 Rust 与 FFmpeg?

1.1 Rust 的优势

  • 内存安全: Rust 的核心特性——所有权(Ownership)和借用(Borrowing)系统,能在编译时消除空指针解引用、数据竞争等内存安全问题,这对于处理复杂且易出错的多媒体数据流至关重要。在长时间运行的服务或处理不可信输入的场景下,这种安全性尤为宝贵。
  • 性能: Rust 被设计为一门高性能语言,其编译产物通常能与 C/C++ 相媲美。它提供零成本抽象,允许开发者编写高级代码而无需担心性能损失。这使得 Rust 非常适合计算密集型的音视频编解码和处理任务。
  • 并发性: Rust 的所有权模型天然地支持无畏并发(Fearless Concurrency)。它可以在编译时防止数据竞争,让开发者能更自信地利用多核处理器并行处理任务,显著提升多媒体处理的吞吐量。
  • 现代化的工具链与生态: Cargo(包管理器和构建工具)、crates.io(中央仓库)以及强大的类型系统和模块化设计,极大地提升了开发效率和项目可维护性。
  • FFI 能力: Rust 具备优秀的与 C 语言库交互的能力(Foreign Function Interface – FFI),这使得直接调用像 FFmpeg 这样用 C 编写的底层库成为可能。

1.2 FFmpeg 的强大

  • 广泛的格式与编解码器支持: FFmpeg 支持几乎所有已知和仍在使用的音视频格式及编解码器,使其成为处理各种来源媒体文件的理想选择。
  • 强大的处理能力: 除了基本的编解码和格式转换,FFmpeg 还提供了海量的滤镜(Filters)用于裁剪、缩放、叠加、调色、降噪、音效处理等,可以通过复杂的滤镜图(Filtergraph)实现高度定制化的处理流程。
  • 跨平台: FFmpeg 可在 Windows, macOS, Linux 等主流操作系统上编译和运行。
  • 成熟稳定: 作为一个发展了二十多年的项目,FFmpeg 经过了广泛的测试和应用,具有很高的稳定性和可靠性。
  • 命令行工具与库: FFmpeg 不仅是一个强大的命令行工具,其核心功能也以一系列共享库(libavcodec, libavformat, libavfilter, libavutil, libswscale, libswresample 等)的形式提供,允许开发者进行更底层的集成。

2. Rust 调用 FFmpeg 的两种主要方法

在 Rust 中利用 FFmpeg 的能力,主要有两种途径:

  1. 通过 std::process::Command 调用 FFmpeg 命令行工具: 这是最简单直接的方法,相当于在 Rust 程序中执行 ffmpeg 命令。
  2. 通过 FFI(Foreign Function Interface)直接调用 FFmpeg 的 C 库: 这是更底层、更灵活但也更复杂的方法,通常借助社区维护的 Rust 绑定库(如 ffmpeg-next)来简化操作。

接下来,我们将详细探讨这两种方法的实现、优缺点及适用场景。

3. 方法一:调用 FFmpeg 命令行工具

这种方法的核心是使用 Rust 的标准库 std::process::Command 来启动和控制一个外部 ffmpeg 进程。

3.1 实现原理

  1. 构建命令: 使用 Command::new("ffmpeg") 创建一个命令构建器。
  2. 添加参数: 使用 .arg().args() 方法添加 FFmpeg 命令所需的参数(如 -i input.mp4, -vf scale=1280:-1, -c:v libx264, output.mkv 等)。
  3. 执行命令:
    • 使用 .status() 执行命令并等待其完成,获取退出状态。
    • 使用 .output() 执行命令,等待完成,并捕获其标准输出(stdout)和标准错误(stderr)。
    • 使用 .spawn() 启动子进程,允许更精细的控制(例如,异步处理输出流)。
  4. 处理结果: 检查命令的退出状态码以判断是否成功。如果使用 .output(),可以解析 stdoutstderr 获取 FFmpeg 的输出信息或错误详情。

3.2 代码示例:简单的视频转码

“`rust
use std::process::{Command, Stdio};
use std::io::{BufReader, BufRead};
use std::path::Path;

fn transcode_video_cli(input_path: &Path, output_path: &Path) -> Result<(), String> {
// 确保 ffmpeg 可执行文件存在
if Command::new(“ffmpeg”).arg(“-version”).output().is_err() {
return Err(“FFmpeg command not found. Make sure FFmpeg is installed and in your PATH.”.to_string());
}

println!("Starting transcoding from {:?} to {:?}", input_path, output_path);

let mut cmd = Command::new("ffmpeg");
cmd.arg("-i")
   .arg(input_path.as_os_str()) // 输入文件
   .arg("-c:v")
   .arg("libx264") // 视频编码器
   .arg("-preset")
   .arg("medium") // 编码速度/质量权衡
   .arg("-crf")
   .arg("23") // 恒定质量因子 (越低质量越好,文件越大)
   .arg("-c:a")
   .arg("aac") // 音频编码器
   .arg("-b:a")
   .arg("128k") // 音频比特率
   .arg("-y") // 覆盖输出文件(如果存在)
   .arg(output_path.as_os_str()); // 输出文件

// 配置 Stderr 管道以捕获 FFmpeg 的进度和错误信息
cmd.stderr(Stdio::piped());

// 启动子进程
let mut child = match cmd.spawn() {
    Ok(child) => child,
    Err(e) => return Err(format!("Failed to spawn FFmpeg process: {}", e)),
};

// 实时读取和打印 FFmpeg 的 stderr 输出 (进度信息等)
if let Some(stderr) = child.stderr.take() {
    let reader = BufReader::new(stderr);
    // 在单独的线程中处理 stderr,避免阻塞主线程
    let handle = std::thread::spawn(move || {
        for line in reader.lines() {
            match line {
                Ok(line_str) => eprintln!("FFmpeg output: {}", line_str), // 使用 eprintln 打印到 stderr
                Err(e) => eprintln!("Error reading FFmpeg stderr: {}", e),
            }
        }
    });
    // 等待 stderr 处理线程结束(可选,取决于是否需要确保所有输出都被处理)
    // handle.join().unwrap();
}


// 等待 FFmpeg 进程完成
let status = match child.wait() {
    Ok(status) => status,
    Err(e) => return Err(format!("Failed to wait for FFmpeg process: {}", e)),
};

if status.success() {
    println!("Transcoding finished successfully.");
    Ok(())
} else {
    Err(format!(
        "FFmpeg process exited with non-zero status: {:?}",
        status.code()
    ))
}

}

fn main() {
let input = Path::new(“input.mp4”); // 替换为你的输入文件
let output = Path::new(“output_cli.mkv”); // 替换为你的输出文件

// // 创建一个简单的测试输入文件(如果需要)
// // 注意:这需要你的系统上安装了 ffmpeg 命令行工具
// if !input.exists() {
//     println!("Creating a dummy input file: input.mp4");
//     let status = Command::new("ffmpeg")
//         .args(&[
//             "-f", "lavfi", "-i", "testsrc=duration=5:size=640x360:rate=30", // 生成5秒测试视频
//             "-f", "lavfi", "-i", "sine=frequency=1000:duration=5",         // 生成5秒正弦波音频
//             "-c:v", "libx264", "-c:a", "aac", "-shortest",
//             input.to_str().unwrap(),
//         ])
//         .status()
//         .expect("Failed to create dummy input file");
//     if !status.success() {
//          eprintln!("Failed to create dummy input file.");
//          return;
//     }
// }


match transcode_video_cli(input, output) {
    Ok(_) => println!("CLI Transcoding Done."),
    Err(e) => eprintln!("CLI Transcoding Error: {}", e),
}

}
“`

3.3 优点

  • 简单易用: 不需要了解 FFmpeg C API 的复杂性,只需熟悉 FFmpeg 命令行参数即可。
  • 快速原型开发: 对于简单的任务,可以非常快速地实现功能。
  • 功能全面: 可以利用 FFmpeg 命令行的所有功能,包括复杂的滤镜链。
  • 隔离性: FFmpeg 进程独立运行,其内部崩溃通常不会直接导致 Rust 应用程序崩溃(除非未处理好进程错误)。

3.4 缺点

  • 性能开销: 每次调用都需要创建新进程,存在进程启动和通信的开销。对于大量短时任务,这可能成为瓶颈。
  • 错误处理困难: 只能通过退出码判断成功与否。详细错误信息需要解析 stderr,这可能比较脆弱,因为 stderr 的格式可能会随 FFmpeg 版本变化。
  • 数据交互不便: 难以直接在 Rust 代码中访问或操作原始的音视频帧数据。如果需要进行内存中的数据处理(如实时分析、与其他库集成),这种方法很不方便。数据通常需要通过文件或管道传递,效率较低。
  • 依赖外部环境: 需要用户的系统上正确安装了 FFmpeg 可执行文件,并且位于 PATH 环境变量中。部署时需要考虑这个依赖。
  • 缺乏类型安全: 命令行参数是字符串,编译器无法检查其正确性。参数错误只会在运行时由 FFmpeg 进程报告。

3.5 适用场景

  • 简单的、一次性的转换任务。
  • 对性能要求不极高,进程启动开销可接受的场景。
  • 需要快速实现功能,且开发者已熟悉 FFmpeg 命令行的项目。
  • 不需要在 Rust 代码中直接操作音视频帧数据的应用。

4. 方法二:通过 FFI 调用 FFmpeg 库

这种方法涉及直接使用 FFmpeg 的 C 语言库(libav* 系列)。由于直接操作 C API 既复杂又不符合 Rust 的安全哲学,通常会使用 Rust 社区提供的绑定库。

  • *-sys crates (e.g., ffmpeg-sys-next): 这些库提供了对 FFmpeg C API 的原始、unsafe 的 Rust 绑定。它们通常由 bindgen 自动生成,提供了函数签名和数据结构的 Rust 定义,但所有调用都在 unsafe 块中进行,需要开发者自行处理内存管理、线程安全和错误检查。
  • 高层封装库 (e.g., ffmpeg-next): 这类库在 *-sys 库之上构建,提供了更安全、更符合 Rust 习惯用法的接口。它们封装了底层的 unsafe 操作,管理资源生命周期(使用 RAII),转换错误码为 Rust 的 Result 类型,并提供更友好的 API。ffmpeg-next 是目前社区中最活跃和推荐的高层封装库之一。

本文将重点介绍使用 ffmpeg-next 的方法。

4.1 环境设置

使用 ffmpeg-next 需要在你的系统上安装 FFmpeg 的开发库(包括头文件和共享/静态库文件)。

  • Linux (Debian/Ubuntu): sudo apt-get install libavcodec-dev libavformat-dev libavfilter-dev libavutil-dev libswscale-dev libswresample-dev pkg-config
  • Linux (Fedora): sudo dnf install ffmpeg-devel pkg-config
  • macOS (Homebrew): brew install ffmpeg pkg-config
  • Windows: 相对复杂,通常需要下载预编译的开发包(例如来自 gyan.devshareddev 包),并配置环境变量(如 FFMPEG_DIR)或使用 vcpkg 等包管理器。

Cargo.toml 中添加依赖:

toml
[dependencies]
ffmpeg-next = "6.0" # 使用当前最新稳定版本
image = "0.24" # 用于保存图像示例
lazy_static = "1.4" # 用于全局初始化

4.2 实现原理与核心概念

使用 FFmpeg 库进行处理通常涉及以下步骤:

  1. 初始化 (Initialization): 全局初始化 FFmpeg 库(通常只需一次)。ffmpeg_next::init().unwrap();
  2. 解复用 (Demuxing): 打开输入文件/流 (ffmpeg_next::format::input()),读取媒体容器格式(如 MP4, MKV, FLV),识别其中的音视频流,并将压缩的数据包(Packet)分离出来。
  3. 查找解码器 (Decoder Finding): 根据数据包中的流信息,找到合适的解码器 (ffmpeg_next::codec::decoder::find())。
  4. 解码 (Decoding): 创建解码器上下文 (ffmpeg_next::codec::Context),将数据包发送给解码器 (decoder.send_packet()),然后从解码器接收解码后的原始帧(Frame – ffmpeg_next::frame::VideoAudio) (decoder.receive_frame())。
  5. 处理/过滤 (Processing/Filtering): (可选)对解码后的原始帧进行处理。可以使用 libavfilter 创建滤镜图(Filtergraph)进行缩放、裁剪、调色等操作,或者直接在 Rust 代码中访问帧数据进行分析、修改。
  6. 编码 (Encoding): (可选,如果需要输出)找到合适的编码器 (ffmpeg_next::codec::encoder::find()),创建编码器上下文,将处理后的原始帧发送给编码器 (encoder.send_frame()),然后从编码器接收编码后的数据包 (encoder.receive_packet())。
  7. 复用 (Muxing): (可选,如果需要输出)创建输出格式上下文 (ffmpeg_next::format::output()),配置输出流,将编码后的数据包写入输出文件/流 (output_context.write_packet())。
  8. 资源清理 (Cleanup): 确保所有分配的上下文、帧、包等资源被正确释放。ffmpeg-next 利用 Rust 的 RAII (Resource Acquisition Is Initialization) 特性,在对象离开作用域时自动调用相应的 avformat_close_input, avcodec_free_context 等函数,大大简化了资源管理。

4.3 代码示例:提取视频第一帧并保存为图片

“`rust
use ffmpeg_next as ffmpeg;
use ffmpeg::format::{input, Pixel};
use ffmpeg::media::Type;
use ffmpeg::software::scaling::{context::Context, flag::Flags};
use ffmpeg::util::frame::video::Video;
use image::{ImageBuffer, Rgb}; // 使用 image crate 来保存图片
use std::path::Path;
use std::fs::File;
use lazy_static::lazy_static;
use std::sync::Once;

static FFMPEG_INIT: Once = Once::new();

// 确保 FFmpeg 只被初始化一次
fn init_ffmpeg() {
FFMPEG_INIT.call_once(|| {
ffmpeg::init().expect(“Failed to initialize FFmpeg”);
println!(“FFmpeg initialized.”);
});
}

fn extract_first_frame(input_path: &Path, output_image_path: &Path) -> Result<(), ffmpeg::Error> {
init_ffmpeg(); // 初始化 FFmpeg

// 1. 解复用:打开输入文件
let mut ictx = input(&input_path)?;

// 2. 查找视频流和解码器
let input_stream = ictx
    .streams()
    .best(Type::Video)
    .ok_or(ffmpeg::Error::StreamNotFound)?;
let video_stream_index = input_stream.index();

let context_decoder = ffmpeg::codec::context::Context::from_parameters(input_stream.parameters())?;
let mut decoder = context_decoder.decoder().video()?;

// 4. 设置 SWScaler 用于可能的格式转换 (转为 RGB24)
let mut scaler = Context::get(
    decoder.format(),
    decoder.width(),
    decoder.height(),
    Pixel::RGB24, // 目标格式
    decoder.width(),
    decoder.height(),
    Flags::BILINEAR,
)?;

let mut frame_count = 0;

// 5. 读取包并解码
'outer_loop: for (stream, packet) in ictx.packets() {
    if stream.index() == video_stream_index {
        // 6. 发送包给解码器
        decoder.send_packet(&packet)?;

        let mut decoded_frame = Video::empty();
        // 7. 接收解码后的帧
        while decoder.receive_frame(&mut decoded_frame).is_ok() {
            println!(
                "Decoded frame {} (type: {:?}, pts: {:?}, format: {:?}, size: {}x{})",
                frame_count,
                decoded_frame.picture_type(),
                decoded_frame.pts(),
                decoded_frame.format(),
                decoded_frame.width(),
                decoded_frame.height()
            );

            // 8. 处理第一帧 (转换并保存)
            if frame_count == 0 {
                let mut rgb_frame = Video::empty();
                scaler.run(&decoded_frame, &mut rgb_frame)?; // 转换为 RGB24

                // 使用 image crate 保存帧
                let img: ImageBuffer<Rgb<u8>, Vec<u8>> = ImageBuffer::from_raw(
                    rgb_frame.width(),
                    rgb_frame.height(),
                    rgb_frame.data(0).to_vec(), // 获取 RGB 数据
                ).expect("Could not create image buffer");

                let mut output_file = File::create(output_image_path)
                    .map_err(|e| ffmpeg::Error::Other { error: Box::new(e) })?; // 将 io::Error 包装成 ffmpeg::Error

                img.write_to(&mut output_file, image::ImageOutputFormat::Png)
                    .map_err(|e| ffmpeg::Error::Other { error: Box::new(e) })?; // 将 image::ImageError 包装

                println!("First frame saved to {:?}", output_image_path);
                break 'outer_loop; // 找到第一帧后退出循环
            }
            frame_count += 1;
        }
        // 如果只想处理第一帧,可以在这里 break 'outer_loop; 避免读取后续包
        // if frame_count > 0 { break 'outer_loop; }
    }
}

// 注意:不需要手动清理资源,ictx, decoder, scaler, decoded_frame, rgb_frame 等
// 在离开作用域时会自动调用相应的 C API 释放函数(由 ffmpeg-next 的 Drop 实现保证)。

if frame_count == 0 {
    println!("No video frames found or decoded.");
    // 可以返回一个特定的错误类型
    Err(ffmpeg::Error::DecoderNotFound) // 或者自定义错误
} else {
    Ok(())
}

}

fn main() {
let input = Path::new(“input.mp4”); // 确保此文件存在
let output_image = Path::new(“first_frame.png”);

// // 创建测试文件(如果需要,且系统中安装了ffmpeg CLI)
// if !input.exists() {
//     println!("Creating a dummy input file: input.mp4");
//     let status = std::process::Command::new("ffmpeg")
//         .args(&[
//             "-f", "lavfi", "-i", "testsrc=duration=5:size=640x360:rate=30",
//             "-f", "lavfi", "-i", "sine=frequency=1000:duration=5",
//             "-c:v", "libx264", "-c:a", "aac", "-shortest",
//             input.to_str().unwrap(),
//         ])
//         .status()
//         .expect("Failed to create dummy input file via CLI");
//     if !status.success() {
//          eprintln!("Failed to create dummy input file.");
//          return;
//     }
// }


match extract_first_frame(input, output_image) {
    Ok(_) => println!("FFI Frame Extraction Done."),
    Err(e) => eprintln!("FFI Frame Extraction Error: {}", e),
}

}
“`

4.4 优点

  • 高性能: 直接在内存中操作数据,避免了进程间通信和磁盘 I/O 的开销,特别适合需要高性能、低延迟的场景。
  • 精细控制: 可以完全控制处理流程的每个步骤,访问原始帧/包数据,实现复杂的自定义逻辑。
  • 内存效率: 数据可以在内存中流转,避免不必要的拷贝和中间文件。
  • 类型安全 (相对 CLI): 使用 ffmpeg-next 等封装库,大部分 API 调用受益于 Rust 的类型系统,编译时能捕获更多错误。
  • 更好的错误处理: ffmpeg-next 将 FFmpeg 的错误码转换为 Rust 的 Result 类型,使得错误处理更加健壮和符合 Rust 习惯。
  • 集成性: 可以方便地将 FFmpeg 的处理能力与其他 Rust 库(如图形库、网络库、AI/ML 库)集成。

4.5 缺点

  • 学习曲线陡峭: 需要理解 FFmpeg 的核心概念(容器、流、包、帧、编解码器、滤镜图等)以及其 C API 的设计哲学(即使通过封装库,底层概念仍然重要)。
  • 复杂性: 代码通常比调用 CLI 更长、更复杂,需要手动管理解码/编码/滤镜等各个环节。
  • unsafe 代码(潜在): 虽然高层封装库隐藏了大部分 unsafe,但在某些高级场景或使用 *-sys 库时,仍可能需要编写 unsafe 代码,并承担相应的维护责任。
  • 构建和依赖管理: 需要正确安装 FFmpeg 开发库,配置链接器。跨平台构建可能比纯 Rust 项目更复杂。静态链接 FFmpeg 库尤其困难。
  • API 稳定性: FFmpeg C API 本身可能发生变化,依赖的绑定库需要及时更新。

4.6 适用场景

  • 需要高性能、低延迟的音视频处理,如实时流处理、视频编辑软件、游戏引擎集成。
  • 需要在 Rust 代码中直接访问、分析或修改音视频帧数据。
  • 需要将 FFmpeg 功能深度集成到大型 Rust 应用中。
  • 开发者愿意投入时间学习 FFmpeg 内部机制以换取更高的性能和灵活性。

5. 如何选择?

选择哪种方法取决于项目的具体需求:

特性 调用 CLI (std::process::Command) 调用 FFI 库 (ffmpeg-next)
简单性 ⭐⭐⭐⭐⭐ ⭐⭐
性能 ⭐⭐ ⭐⭐⭐⭐⭐
控制粒度 ⭐⭐ ⭐⭐⭐⭐⭐
内存效率 ⭐⭐ ⭐⭐⭐⭐
错误处理 ⭐⭐ ⭐⭐⭐⭐
集成性 ⭐⭐ ⭐⭐⭐⭐⭐
依赖管理 ⭐⭐⭐⭐ (依赖运行时) ⭐⭐ (依赖开发库,构建复杂)
学习曲线 ⭐⭐⭐⭐ (需懂 FFmpeg CLI) ⭐⭐ (需懂 FFmpeg C API 概念)

总结:

  • 选择 CLI 方法: 如果你的任务相对简单(如格式转换、基本裁剪),对性能要求不是极致,希望快速实现,并且可以接受对外部 FFmpeg 可执行文件的依赖。
  • 选择 FFI 方法: 如果你需要高性能、低延迟、精细控制,需要在内存中处理帧数据,或者要将 FFmpeg 功能深度集成到 Rust 应用中,并且愿意投入时间学习 FFmpeg 库。

6. 进阶考量

无论选择哪种方法,还有一些共通的进阶话题值得关注:

  • 错误处理: 即便使用 FFI,FFmpeg 的错误有时也比较晦涩。需要建立良好的错误处理和日志记录机制。对于 CLI,要健壮地解析 stderr。
  • 异步处理: 对于长时间运行的任务(如转码大文件),应使用异步方式避免阻塞主线程。
    • CLI: 可以使用 tokio::process::Command 配合 async/await
    • FFI: 需要更小心地设计,确保 FFmpeg 库调用不会阻塞异步运行时,可能需要将阻塞调用放在 tokio::task::spawn_blocking 中执行。
  • 资源管理: FFI 方法中,虽然 ffmpeg-next 提供了 RAII,但仍需理解资源生命周期,避免悬垂指针或过早释放。对于 CLI,要确保子进程在程序退出或出错时能被正确终止。
  • 线程安全: FFmpeg 的某些部分不是线程安全的。使用 FFI 时,需要查阅文档或库的说明,了解哪些操作可以在多线程中安全进行,必要时使用互斥锁(Mutex)等同步原语。
  • 硬件加速: FFmpeg 支持通过 VAAPI (Linux), VDPAU (Linux), NVENC/NVDEC (NVIDIA), VideoToolbox (macOS), QSV (Intel) 等进行硬件加速编解码。通过 FFI 调用可以利用这些特性,但配置和使用会更复杂。CLI 也可通过参数启用硬件加速。
  • 滤镜 (Filtergraphs): FFmpeg 的滤镜系统非常强大。
    • CLI: 通过 -vf-af 参数指定滤镜链。
    • FFI: 使用 libavfilter API 构建和运行滤镜图。ffmpeg-next 对此也有封装。

7. 结语

Rust 与 FFmpeg 的结合为开发者提供了构建高性能、安全可靠的多媒体处理应用的强大武器。通过命令行调用提供了简单快捷的途径,适合快速原型和简单任务;而通过 FFI(尤其是借助 ffmpeg-next 这样的库)则打开了通往高性能、精细控制和深度集成的大门,尽管学习曲线更陡峭。

理解这两种方法的优劣和适用场景,根据项目需求做出明智的选择,并掌握相关的核心概念和最佳实践,将使你能够充分利用 Rust 的现代语言特性和 FFmpeg 无与伦比的多媒体处理能力,创造出色的音视频应用。随着 Rust 生态的不断成熟和 ffmpeg-next 等库的持续发展,我们有理由相信,Rust 将在多媒体处理领域扮演越来越重要的角色。


发表评论

您的邮箱地址不会被公开。 必填项已用 * 标注

滚动至顶部