如何手动将H264封装为MP4?基于Rust的ffmpeg-the-third实现求助
问题描述
在Windows平台通过Desktop Duplication API捕获原始BGRA格式屏幕帧,已用x264 crate编码为H264裸流,现在需要将H264帧封装为MP4文件,尝试使用Rust的ffmpeg-the-third库但无法推进,寻求实现指导或示例。
已有的x264编码代码:
fn encode_frames(frames: Vec<Vec<u8>>, width: u32, height: u32, fps: u32) -> Result<()> { let mut file = File::create("../vid/captured_video.h264").unwrap(); let mut encoder = Encoder::builder() .fps(fps, 1) .build(Colorspace::BGRA, width as _, height as _) .unwrap(); { let headers = encoder.headers().unwrap(); file.write_all(headers.entirety()).unwrap(); } for (index, mut pic_data) in frames.into_iter().enumerate() { let image = Image::bgra(width as _, height as _, &pic_data); let (data, _) = encoder.encode((fps as usize * index) as _, image).unwrap(); file.write_all(data.entirety()).unwrap(); } // Flush out delayed frames { let mut flush = encoder.flush(); while let Some(result) = flush.next() { let (data, _) = result.unwrap(); file.write_all(data.entirety()).unwrap(); } } Ok(()) }
方案一:使用ffmpeg-the-third手动封装MP4
步骤说明
- 初始化FFmpeg库并注册所有格式/编码器
- 创建MP4输出上下文,配置输出格式
- 添加视频流并设置核心参数(分辨率、帧率、像素格式等)
- 写入MP4文件头
- 分割H264裸流的NALU单元,打包为FFmpeg数据包并写入
- 写入文件尾完成封装
实现代码
首先在Cargo.toml添加依赖:
[dependencies] ffmpeg-the-third = "0.4" x264 = "0.15" anyhow = "1.0"
封装函数实现:
use ffmpeg_the_third::{ codec::{context::Context as CodecContext, packet::Packet}, format::{context::Output as OutputContext, stream::Stream}, util::{Rational}, codec, format, Dictionary, }; use std::fs::File; use std::path::Path; use anyhow::{Result, Context}; fn h264_to_mp4(h264_path: &str, mp4_path: &str, width: u32, height: u32, fps: u32) -> Result<()> { // 初始化FFmpeg ffmpeg_the_third::init().context("FFmpeg初始化失败")?; // 打开H264裸流文件 let mut h264_file = File::open(h264_path).context("打开H264文件失败")?; // 创建MP4输出上下文 let mut output_ctx = OutputContext::new(Path::new(mp4_path), None, None) .context("创建MP4输出上下文失败")?; // 配置视频流编码器 let codec = codec::find_by_name("libx264") .context("找不到H264编码器")? .encoder() .video() .context("获取H264视频编码器失败")?; let mut stream = output_ctx.add_stream(Some(&codec)) .context("添加视频流失败")?; // 设置编码器核心参数 let mut codec_ctx = stream.codec_context(); codec_ctx.set_width(width as i32); codec_ctx.set_height(height as i32); codec_ctx.set_frame_rate(Rational::new(fps as i32, 1)); codec_ctx.set_time_base(Rational::new(1, fps as i32)); codec_ctx.set_format(codec::pixel::Format::YUV420P); // MP4标准像素格式 // MP4要求全局头,设置对应标志 if output_ctx.format().flags().contains(format::flag::GLOBAL_HEADER) { codec_ctx.set_flags(codec::flag::GLOBAL_HEADER); } // 打开编码器上下文 codec_ctx.open(None, &mut Dictionary::new()) .context("打开编码器上下文失败")?; // 同步流参数到输出上下文 stream.set_parameters(codec_ctx.parameters()); // 写入MP4文件头 output_ctx.write_header(&mut Dictionary::new()) .context("写入MP4文件头失败")?; // 读取并分割H264裸流的NALU单元 let mut buffer = Vec::new(); let mut nalu_buffer = Vec::new(); let mut timestamp = 0; let time_base = stream.time_base(); loop { let mut buf = [0u8; 4096]; let n = h264_file.read(&mut buf).context("读取H264文件失败")?; if n == 0 { break; } buffer.extend_from_slice(&buf[0..n]); // 查找NALU起始码(0x000001或0x00000001) while let Some(pos) = find_nalu_start(&buffer) { if pos > 0 { nalu_buffer.extend_from_slice(&buffer[0..pos]); // 封装NALU为FFmpeg数据包 let mut packet = Packet::new(); packet.data_mut().extend_from_slice(&nalu_buffer); packet.set_stream(stream.index() as i32); packet.set_pts(timestamp as i64); packet.set_dts(timestamp as i64); packet.set_duration(time_base.den() as i64 / time_base.num() as i64); packet.set_time_base(time_base); // 写入数据包到MP4 output_ctx.write_packet(&mut packet) .context("写入数据包失败")?; timestamp += 1; nalu_buffer.clear(); } // 移除起始码(兼容3字节/4字节起始码) buffer.drain(0..pos+3); if buffer.starts_with(&[0x00]) { buffer.drain(0..1); } } } // 处理剩余的NALU数据 if !nalu_buffer.is_empty() { let mut packet = Packet::new(); packet.data_mut().extend_from_slice(&nalu_buffer); packet.set_stream(stream.index() as i32); packet.set_pts(timestamp as i64); packet.set_dts(timestamp as i64); packet.set_duration(time_base.den() as i64 / time_base.num() as i64); packet.set_time_base(time_base); output_ctx.write_packet(&mut packet) .context("写入最终数据包失败")?; } // 写入MP4文件尾 output_ctx.write_trailer() .context("写入MP4文件尾失败")?; Ok(()) } // 辅助函数:查找H264 NALU起始位置 fn find_nalu_start(buffer: &[u8]) -> Option<usize> { if buffer.len() < 3 { return None; } // 查找3字节起始码0x000001 for i in 0..buffer.len()-2 { if buffer[i] == 0x00 && buffer[i+1] == 0x00 && buffer[i+2] == 0x01 { return Some(i); } } // 查找4字节起始码0x00000001 if buffer.len() >=4 { for i in 0..buffer.len()-3 { if buffer[i] ==0x00 && buffer[i+1]==0x00 && buffer[i+2]==0x00 && buffer[i+3]==0x01 { return Some(i); } } } None }
在encode_frames执行完成后调用封装函数:
// 假设frames、width、height、fps已定义 encode_frames(frames, width, height, fps)?; h264_to_mp4("../vid/captured_video.h264", "../vid/captured_video.mp4", width, height, fps)?;
方案二:调用ffmpeg命令行快速封装
如果不需要手动实现封装逻辑,可直接调用系统中的ffmpeg命令行(需确保ffmpeg已安装并加入PATH):
use std::process::Command; use anyhow::Result; fn h264_to_mp4_cli(h264_path: &str, mp4_path: &str, fps: u32) -> Result<()> { Command::new("ffmpeg") .arg("-framerate") .arg(fps.to_string()) .arg("-i") .arg(h264_path) .arg("-c:v") .arg("copy") // 直接复制流,无重新编码开销 .arg(mp4_path) .status() .context("执行ffmpeg命令失败")?; Ok(()) }
关键注意事项
- 时间戳处理:MP4依赖正确的pts/dts时间戳保证播放流畅,需严格匹配帧率设置time_base和duration。
- NALU分割:x264输出的H264裸流是带起始码的NALU序列,必须分割后才能正确封装。
- 全局头设置:MP4格式要求编码器输出全局头,需设置
GLOBAL_HEADER标志。
内容的提问来源于stack exchange,提问作者Kai
相关产品推荐
相关产品推荐

