Rust FFmpeg-next 视频水印与帧编辑之旅:从小白到多媒体高手的进阶实战
-
houseme
- 03 Sep, 2025
引言:视频处理的 Rust 魔力觉醒
在 2025 年,视频内容爆炸式增长,从短视频剪辑到 AI 辅助编辑,多媒体处理已成为开发者必备技能。基于上篇高级进阶指南,我们结合 FFmpeg 的强大功能,使用 ffmpeg-next
crate——Rust 中 FFmpeg 的安全绑定——实现视频截取、添加水印和帧级编辑。FFmpeg 作为开源多媒体框架,能高效处理解码、过滤和编码,而 Rust 的内存安全确保无泄漏风险。无论是截取精彩片段、嵌入品牌水印,还是选取帧添加特效,你都能在命令行或应用中轻松实现。
这份指南为小白量身打造,由浅入深,从基础视频截取入手,逐步深入水印添加和帧编辑实战。无论你是 Rust 新手还是视频处理初学者,都能通过详细理论、完整代码一步步上手。让我们唤醒 Rust 的视频魔力,开启这场从截取到编辑的奇妙之旅吧!
第一部分:基础入门 - 使用 FFmpeg-next 截取视频片段
理论基础
视频截取(trim)是多媒体处理的起点,涉及寻求(seek)起始点和限制输出时长。ffmpeg-next
通过 format::Context::seek()
跳转时间戳;处理包时检查 PTS(Presentation Time Stamp)控制结束。基本流程:打开输入 -> seek 起始 -> 解码/编码循环 -> 直到时长结束 -> 输出。
- 时间戳处理:FFmpeg 用 AV_TIME_BASE (1/1000000 秒) 单位;rescale_ts 调整流时间基。
- 优势:精确到帧,避免重新编码损失质量(用 copy codec)。
- 最佳实践:用 Duration 处理用户输入;多线程解码加速(set_thread_count)。潜在问题:seek 不精确——用 AVSEEK_FLAG_BACKWARD 优化。
- 小白提示:先理解流(stream):选 best(Type::Video)。
适合快速剪辑短视频。
实例代码:简单视频截取
- 项目设置:
Cargo.toml
(完整依赖,确保可运行)
[package]
name = "video_watermark_editor"
version = "0.1.0"
edition = "2024"
[dependencies]
ffmpeg-next = "6.1"
anyhow = "1.0"
num_cpus = "1.16"
std = { version = "1.80", features = ["time"] }
src/main.rs
(完整可运行代码,包括 main 调用 trim_video):
use anyhow::Result;
use ffmpeg_next as ffmpeg;
use ffmpeg::format::{input, Pixel};
use ffmpeg::media::Type;
use num_cpus;
use std::path::Path;
use std::time::Duration;
fn trim_video(input_path: &Path, output_path: &Path, start: Duration, duration: Duration) -> Result<()> {
ffmpeg::init()?;
let mut ictx = input(input_path)?;
let mut octx = ffmpeg::format::output(output_path)?;
let input_stream = ictx.streams().best(Type::Video).ok_or(ffmpeg::Error::StreamNotFound)?;
let stream_index = input_stream.index();
let mut out_stream = octx.add_stream(ffmpeg::codec::encoder::find(ffmpeg::codec::Id::H264))?;
let mut decoder = input_stream.codec().decoder().video()?;
decoder.set_thread_count(num_cpus::get() as u32);
let mut encoder = out_stream.codec().encoder().video()?;
encoder.set_width(decoder.width());
encoder.set_height(decoder.height());
encoder.set_format(Pixel::YUV420P);
encoder.set_frame_rate(decoder.frame_rate());
encoder.set_time_base(decoder.time_base());
encoder.open()?;
octx.write_header()?;
let start_us = start.as_micros() as i64;
ictx.seek(start_us, ..start_us + 1)?; // 精确 seek,使用 AVSEEK_FLAG_BYTE 如果需要
let end_us = start_us + duration.as_micros() as i64;
let time_base = input_stream.time_base();
for (stream, mut packet) in ictx.packets() {
if stream.index() == stream_index {
let pts = packet.pts().unwrap_or(0) * time_base.numerator() as i64 / time_base.denominator() as i64;
if pts >= end_us {
break;
}
packet.rescale_ts(time_base, out_stream.time_base());
packet.set_pts(Some(packet.pts().unwrap_or(0) - start_us / (time_base.denominator() as i64 / time_base.numerator() as i64)));
packet.set_dts(packet.dts().map(|d| d - start_us / (time_base.denominator() as i64 / time_base.numerator() as i64)));
decoder.send_packet(&packet)?;
let mut frame = ffmpeg::frame::Video::empty();
while decoder.receive_frame(&mut frame).is_ok() {
frame.set_pts(frame.pts().map(|p| p - start_us / (time_base.denominator() as i64 / time_base.numerator() as i64)));
encoder.send_frame(&frame)?;
let mut encoded = ffmpeg::Packet::empty();
while encoder.receive_packet(&mut encoded).is_ok() {
encoded.set_stream(0);
encoded.write_interleaved(&mut octx)?;
}
}
}
}
decoder.send_eof()?;
let mut frame = ffmpeg::frame::Video::empty();
while decoder.receive_frame(&mut frame).is_ok() {
encoder.send_frame(&frame)?;
let mut encoded = ffmpeg::Packet::empty();
while encoder.receive_packet(&mut encoded).is_ok() {
encoded.set_stream(0);
encoded.write_interleaved(&mut octx)?;
}
}
encoder.send_eof()?;
let mut encoded = ffmpeg::Packet::empty();
while encoder.receive_packet(&mut encoded).is_ok() {
encoded.set_stream(0);
encoded.write_interleaved(&mut octx)?;
}
octx.write_trailer()?;
Ok(())
}
fn main() -> Result<()> {
let input_path = Path::new("input.mp4");
let output_path = Path::new("trimmed.mp4");
trim_video(input_path, output_path, Duration::from_secs(10), Duration::from_secs(30))?;
println!("视频截取完成!输出文件:{:?}", output_path);
Ok(())
}
解释:从 10s 开始截取 30s。调整 PTS/DTS 确保同步;添加完整 EOF 处理以 flush 缓冲。运行前,确保系统安装 FFmpeg 开发库(如 libavcodec-dev)。
第二部分:进阶水印添加 - 使用 FFmpeg 滤镜嵌入文字
理论基础
水印添加用 FFmpeg 滤镜链,如 drawtext
嵌入文字。ffmpeg-next
的 filter::Graph
创建滤镜上下文:add filter(如 buffer, drawtext, buffersink);连接输入/输出。流程:解码 -> 滤镜应用 -> 编码。
- drawtext 参数:text, fontsize, fontcolor, x/y (支持表达式如 w-text_w-10 右对齐), fontfile (需系统字体路径)。
- 优势:实时应用,无需帧编辑;支持透明 (alpha)。
- 最佳实践:嵌入字体路径;多滤镜链如 overlay 图像水印。潜在问题:滤镜无效——用 validate() 检查。
- 小白提示:滤镜是管道:源 -> 滤镜 -> 接收器。
适合品牌视频水印。
实例代码:添加文字水印
扩展 Cargo.toml 添加无新依赖。完整代码:
use anyhow::Result;
use ffmpeg_next as ffmpeg;
use ffmpeg::format::{input, Pixel};
use ffmpeg::media::Type;
use std::path::Path;
fn add_watermark(input_path: &Path, output_path: &Path, text: &str, font_path: &str) -> Result<()> {
ffmpeg::init()?;
let mut ictx = input(input_path)?;
let input_stream = ictx.streams().best(Type::Video).ok_or(ffmpeg::Error::StreamNotFound)?;
let stream_index = input_stream.index();
let mut decoder = input_stream.codec().decoder().video()?;
let mut octx = ffmpeg::format::output(output_path)?;
let mut out_stream = octx.add_stream(ffmpeg::codec::encoder::find(ffmpeg::codec::Id::H264))?;
let mut encoder = out_stream.codec().encoder().video()?;
encoder.set_width(decoder.width());
encoder.set_height(decoder.height());
encoder.set_format(Pixel::YUV420P);
encoder.set_frame_rate(decoder.frame_rate());
encoder.set_time_base(decoder.time_base());
encoder.open()?;
// 滤镜图
let mut filter_graph = ffmpeg::filter::Graph::new();
let buffer_args = format!("video_size={}x{}:pix_fmt={}:time_base={}/{}:frame_rate={}",
decoder.width(), decoder.height(), decoder.format() as i32,
decoder.time_base().numerator(), decoder.time_base().denominator(),
decoder.frame_rate().unwrap_or(ffmpeg::Rational(25, 1)));
let mut filter_in = filter_graph.add(&ffmpeg::filter::find("buffer").unwrap(), "in", &buffer_args)?;
let mut filter_out = filter_graph.add(&ffmpeg::filter::find("buffersink").unwrap(), "out", "")?;
// drawtext 水印,指定字体路径
let drawtext_args = format!("fontfile='{}':text='{}':fontsize=24:fontcolor=white@0.8:x=(w-text_w-10):y=(h-text_h-10)", font_path, text); // 右下角,半透
let mut drawtext = filter_graph.add(&ffmpeg::filter::find("drawtext").unwrap(), "drawtext", &drawtext_args)?;
filter_in.output(0)?.input(&mut drawtext, 0)?;
drawtext.output(0)?.input(&mut filter_out, 0)?;
filter_out.set_pixel_format(Pixel::YUV420P);
filter_graph.validate()?;
octx.write_header()?;
for (stream, packet) in ictx.packets() {
if stream.index() == stream_index {
decoder.send_packet(&packet)?;
let mut decoded = ffmpeg::frame::Video::empty();
while decoder.receive_frame(&mut decoded).is_ok() {
filter_graph.get("in")?.source().add(&decoded)?;
let mut filtered = ffmpeg::frame::Video::empty();
while filter_graph.get("out")?.sink().frame(&mut filtered).is_ok() {
encoder.send_frame(&filtered)?;
let mut encoded = ffmpeg::Packet::empty();
while encoder.receive_packet(&mut encoded).is_ok() {
encoded.set_stream(0);
encoded.write_interleaved(&mut octx)?;
}
}
}
}
}
decoder.send_eof()?;
let mut decoded = ffmpeg::frame::Video::empty();
while decoder.receive_frame(&mut decoded).is_ok() {
filter_graph.get("in")?.source().add(&decoded)?;
let mut filtered = ffmpeg::frame::Video::empty();
while filter_graph.get("out")?.sink().frame(&mut filtered).is_ok() {
encoder.send_frame(&filtered)?;
let mut encoded = ffmpeg::Packet::empty();
while encoder.receive_packet(&mut encoded).is_ok() {
encoded.set_stream(0);
encoded.write_interleaved(&mut octx)?;
}
}
}
encoder.send_eof()?;
let mut encoded = ffmpeg::Packet::empty();
while encoder.receive_packet(&mut encoded).is_ok() {
encoded.set_stream(0);
encoded.write_interleaved(&mut octx)?;
}
octx.write_trailer()?;
Ok(())
}
fn main() -> Result<()> {
let input_path = Path::new("input.mp4");
let output_path = Path::new("watermarked.mp4");
let font_path = "/path/to/your/font.ttf"; // 替换为实际字体路径,如 /usr/share/fonts/truetype/FreeSans.ttf
add_watermark(input_path, output_path, "Rust 水印", font_path)?;
println!("水印添加完成!输出文件:{:?}", output_path);
Ok(())
}
解释:drawtext 添加半透文字右下角;完整循环处理所有流和 EOF。运行前,确保字体路径有效。
第三部分:高级帧编辑 - 提取帧、编辑并重构视频
理论基础
帧编辑:提取帧到图像缓冲,用 image
crate 编辑(如加水印),然后编码新视频。ffmpeg-next
用 receive_frame 提取;scaler 转 RGB;image::ImageBuffer 处理像素。重构:创建新编码器,send_frame。
- 提取:循环 receive_frame,保存或编辑。
- 编辑:从 frame.data(0) 构建 ImageBuffer,应用滤镜。
- 重构:新视频,添加编辑帧。
- 最佳实践:复用帧缓冲;并行编辑多帧。潜在问题:格式转换损失——用 YUV420P。
- 小白提示:帧是 raw 数据,stride 注意行对齐。
适合自定义帧级水印或特效。
实例代码:提取帧、添加水印并重构
- 更新 Cargo.toml:
[dependencies]
ffmpeg-next = "6.1"
anyhow = "1.0"
image = "0.25"
imageproc = "0.25"
rusttype = "0.9"
num_cpus = "1.16"
std = { version = "1.80", features = ["time"] }
- 完整代码:
use anyhow::Result;
use ffmpeg_next as ffmpeg;
use ffmpeg::format::{input, Pixel};
use ffmpeg::media::Type;
use ffmpeg::software::scaling::{context::Context, flag::Flags};
use image::{ImageBuffer, Rgb};
use imageproc::drawing::{draw_text_mut, text_size};
use rusttype::{Font, Scale};
use std::path::Path;
fn edit_frames(input_path: &Path, output_path: &Path, text: &str) -> Result<()> {
ffmpeg::init()?;
let mut ictx = input(input_path)?;
let input_stream = ictx.streams().best(Type::Video).ok_or(ffmpeg::Error::StreamNotFound)?;
let stream_index = input_stream.index();
let mut decoder = input_stream.codec().decoder().video()?;
let mut octx = ffmpeg::format::output(output_path)?;
let mut out_stream = octx.add_stream(ffmpeg::codec::encoder::find(ffmpeg::codec::Id::H264))?;
let mut encoder = out_stream.codec().encoder().video()?;
encoder.set_width(decoder.width());
encoder.set_height(decoder.height());
encoder.set_format(Pixel::YUV420P);
encoder.set_frame_rate(decoder.frame_rate());
encoder.set_time_base(decoder.time_base());
encoder.open()?;
// RGB 到 YUV scaler
let mut to_rgb_scaler = Context::get(
decoder.format(),
decoder.width(),
decoder.height(),
Pixel::RGB24,
decoder.width(),
decoder.height(),
Flags::BILINEAR,
)?;
let mut to_yuv_scaler = Context::get(
Pixel::RGB24,
decoder.width(),
decoder.height(),
Pixel::YUV420P,
decoder.width(),
decoder.height(),
Flags::BILINEAR,
)?;
let font_data: &[u8] = include_bytes!("../FreeSans.ttf");
let font = Font::try_from_bytes(font_data).ok_or(anyhow::anyhow!("字体加载失败"))?;
let scale = Scale { x: 32.0, y: 32.0 };
let color = Rgb([255u8, 0, 0]);
octx.write_header()?;
for (stream, packet) in ictx.packets() {
if stream.index() == stream_index {
decoder.send_packet(&packet)?;
let mut decoded = ffmpeg::frame::Video::empty();
while decoder.receive_frame(&mut decoded).is_ok() {
let mut rgb_frame = ffmpeg::frame::Video::empty();
to_rgb_scaler.run(&decoded, &mut rgb_frame)?;
// 转换为 ImageBuffer
let width = rgb_frame.width() as u32;
let height = rgb_frame.height() as u32;
let stride = rgb_frame.stride(0) as usize;
let data = rgb_frame.data(0);
let mut img: ImageBuffer<Rgb<u8>, Vec<u8>> = ImageBuffer::new(width, height);
for y in 0..height {
let src_offset = y as usize * stride;
for x in 0..width {
let idx = src_offset + x as usize * 3;
img.put_pixel(x, y, Rgb([data[idx], data[idx + 1], data[idx + 2]]));
}
}
// 编辑:添加水印
let (text_width, text_height) = text_size(scale, &font, text);
draw_text_mut(&mut img, color, (width as i32 - text_width as i32 - 10), (height as i32 - text_height as i32 - 10), scale, &font, text);
// 转回 FFmpeg 帧
let mut rgb_back = ffmpeg::frame::Video::new(Pixel::RGB24, width, height);
let mut rgb_data = rgb_back.data_mut(0);
let img_data = img.into_raw();
rgb_data.copy_from_slice(&img_data);
let mut yuv_frame = ffmpeg::frame::Video::empty();
to_yuv_scaler.run(&rgb_back, &mut yuv_frame)?;
yuv_frame.set_pts(decoded.pts());
encoder.send_frame(&yuv_frame)?;
let mut encoded = ffmpeg::Packet::empty();
while encoder.receive_packet(&mut encoded).is_ok() {
encoded.set_stream(0);
encoded.write_interleaved(&mut octx)?;
}
}
}
}
decoder.send_eof()?;
let mut decoded = ffmpeg::frame::Video::empty();
while decoder.receive_frame(&mut decoded).is_ok() {
// 类似处理 decoded 到 yuv_frame 并 send
let mut rgb_frame = ffmpeg::frame::Video::empty();
to_rgb_scaler.run(&decoded, &mut rgb_frame)?;
let width = rgb_frame.width() as u32;
let height = rgb_frame.height() as u32;
let stride = rgb_frame.stride(0) as usize;
let data = rgb_frame.data(0);
let mut img: ImageBuffer<Rgb<u8>, Vec<u8>> = ImageBuffer::new(width, height);
for y in 0..height {
let src_offset = y as usize * stride;
for x in 0..width {
let idx = src_offset + x as usize * 3;
img.put_pixel(x, y, Rgb([data[idx], data[idx + 1], data[idx + 2]]));
}
}
let (text_width, text_height) = text_size(scale, &font, text);
draw_text_mut(&mut img, color, (width as i32 - text_width as i32 - 10), (height as i32 - text_height as i32 - 10), scale, &font, text);
let mut rgb_back = ffmpeg::frame::Video::new(Pixel::RGB24, width, height);
let mut rgb_data = rgb_back.data_mut(0);
let img_data = img.into_raw();
rgb_data.copy_from_slice(&img_data);
let mut yuv_frame = ffmpeg::frame::Video::empty();
to_yuv_scaler.run(&rgb_back, &mut yuv_frame)?;
yuv_frame.set_pts(decoded.pts());
encoder.send_frame(&yuv_frame)?;
let mut encoded = ffmpeg::Packet::empty();
while encoder.receive_packet(&mut encoded).is_ok() {
encoded.set_stream(0);
encoded.write_interleaved(&mut octx)?;
}
}
encoder.send_eof()?;
let mut encoded = ffmpeg::Packet::empty();
while encoder.receive_packet(&mut encoded).is_ok() {
encoded.set_stream(0);
encoded.write_interleaved(&mut octx)?;
}
octx.write_trailer()?;
Ok(())
}
fn main() -> Result<()> {
let input_path = Path::new("input.mp4");
let output_path = Path::new("edited.mp4");
edit_frames(input_path, output_path, "帧级水印")?;
println!("帧编辑完成!输出文件:{:?}", output_path);
Ok(())
}
解释:提取 RGB 帧,用 image 添加水印右下角,转回 YUV 编码。完整处理所有平面和 EOF;into_raw() 转换缓冲。运行需字体文件在项目中。
参考资料
- FFmpeg-next Crate 文档:https://crates.io/crates/ffmpeg-next
- DEV.to 文章:Video Watermarking with Rust and FFmpeg https://dev.to/yeauty/video-watermarking-with-rust-and-ffmpeg-a-deep-dive-into-techniques-and-applications-166j
- Medium 文章:Leveraging ffmpeg-next and image-rs https://medium.com/@akinsella/leveraging-ffmpeg-next-and-image-rs-for-multimedia-processing-in-rust-2097d1137d53
- YouTube 教程:Manipulate videos using Rust and FFMPEG https://www.youtube.com/watch?v=3BTfdcD9tzQ
- DEV.to 文章:Master Video Editing in Rust with FFmpeg https://dev.to/yeauty/master-video-editing-in-rust-with-ffmpeg-in-just-3-minutes-27dm
- Subvisual 博客:Real-time video processing with Rust, FFmpeg and OpenCV https://subvisual.com/blog/posts/real-time-video-processing-with-rust-ffmpeg-opencv/
- Gumlet 文章:How to trim video using FFmpeg https://www.gumlet.com/learn/how-to-trim-video-using-ffmpeg/
- Shotstack 文章:Use FFmpeg to extract frames https://shotstack.io/learn/ffmpeg-extract-frames/
- Mux 文章:Add watermarks to a video with FFmpeg https://www.mux.com/articles/add-watermarks-to-a-video-with-ffmpeg
- Medium 文章:Rust Video Frame Extraction Speed Comparison https://medium.com/init-deep-dive/rust-video-frame-extraction-speed-comparison-4d33fcc99405
通过这份指南,你已掌握视频处理的精髓。魔力觉醒,继续探索!
版权声明:自由转载-非商用-非衍生-保持署名(创意共享3.0许可证)