You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何手动将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

步骤说明

  1. 初始化FFmpeg库并注册所有格式/编码器
  2. 创建MP4输出上下文,配置输出格式
  3. 添加视频流并设置核心参数(分辨率、帧率、像素格式等)
  4. 写入MP4文件头
  5. 分割H264裸流的NALU单元,打包为FFmpeg数据包并写入
  6. 写入文件尾完成封装

实现代码

首先在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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.16 19:27:46