4、视频流提取:从MP4中提取H.264/H.265裸流、提取时保持原始编码参数

好,咱们进入第四讲。这一讲的主题很明确——从MP4里把视频裸流提出来。说白了,就是把那个封装壳子剥掉,只留下纯纯的视频编码数据。

为什么要干这事儿?我遇到过不少场景:比如你要做码流分析、要喂给硬件解码器、或者要做二次转封装。这时候,带着MP4的moov盒子反而碍事。裸流,才是最干净的数据。

4.1 什么是裸流?为什么需要它?

先搞清楚概念。H.264裸流,通常以 0x00 0x00 0x00 0x01 开头,后面跟着NAL单元。H.265也一样,只是NAL单元类型定义不同。

MP4里的视频轨道,其实也是这些NAL单元。只不过它们被切碎、打包、塞进了mdat盒子。提取裸流,就是把这些碎片重新拼起来,还原成完整的字节流。

你想想看,如果直接拿FFmpeg的 -c copy 输出,默认会带上MP4的封装。但我们要的是 .h264.hevc 文件,里面只有起始码+NAL数据。

核心要点:提取裸流时,必须保留原始编码参数(SPS/PPS/VPS)。否则解码器不认识你的码流,直接罢工。

4.2 基础命令:一行搞定

先来个最直接的。假设你有一个 input.mp4,里面是H.264视频。

ffmpeg -i input.mp4 -c:v copy -bsf:v h264_mp4toannexb output.h264

这条命令干了三件事:

  • -c:v copy:视频流直接拷贝,不解码不重编码。速度最快,质量无损。
  • -bsf:v h264_mp4toannexb:比特流过滤器。把MP4格式的NAL单元(带长度前缀)转换成Annex B格式(带起始码)。
  • output.h264:输出裸流文件。扩展名不重要,但约定俗成用 .h264.264

如果是H.265,把过滤器换成 hevc_mp4toannexb

ffmpeg -i input.mp4 -c:v copy -bsf:v hevc_mp4toannexb output.h265

嗯,这里要注意:有些MP4文件里的H.265,其实叫HEVC。但FFmpeg里统一用 hevc 这个编解码器名称。别写成 h265,会报错。

4.3 深入理解:比特流过滤器在干什么?

为什么需要这个过滤器?我刚开始做的时候也纳闷——直接copy不行吗?

不行。MP4里存储NAL单元的方式,和裸流不一样:

封装格式 NAL单元分隔方式 示例
MP4 4字节长度前缀(大端) 00 00 03 21 [NAL数据]
Annex B(裸流) 起始码 00 00 00 01 00 00 00 01 [NAL数据]

MP4里每个NAL单元前面,写的是这个单元的长度。解码器读到长度,就知道要读多少字节。而裸流里,用固定的起始码来标记边界。

所以,h264_mp4toannexb 这个过滤器,就是把长度前缀替换成起始码。同时,它还会做一件重要的事——把SPS和PPS提取出来,插入到关键帧前面。

个人经验:我曾经遇到一个MP4文件,里面的SPS/PPS只出现了一次,在文件开头。如果不用这个过滤器,直接copy出来的裸流,后面的关键帧就没有SPS/PPS。解码器读到第二个关键帧就崩了。这个过滤器会自动在每个IDR帧前插入SPS/PPS,保证码流完整。

4.4 保持原始编码参数:不只是SPS/PPS

「保持原始编码参数」这句话,听起来简单,做起来有坑。

原始编码参数包括:

  • SPS(序列参数集):分辨率、帧率、档次、级别等
  • PPS(图像参数集):熵编码模式、分片数量等
  • VPS(视频参数集,仅H.265):分层编码信息
  • SEI(补充增强信息):可能包含色彩空间、HDR元数据等

-bsf:v h264_mp4toannexb 时,默认会保留这些参数。但有一个例外——如果原始MP4里,SPS/PPS被放在了moov盒子的 avcChvcC 中,而不是在视频数据流里,过滤器会从这些box中读取并插入。

我建议你验证一下:

ffprobe -v error -show_entries stream=codec_name,codec_tag_string,profile,level,coded_width,coded_height input.mp4

提取后,再用同样的命令检查裸流文件:

ffprobe -v error -show_entries stream=codec_name,profile,level,coded_width,coded_height output.h264

如果参数一致,说明提取成功。如果不一致,八成是过滤器没生效,或者原始文件有问题。

避坑指南:我曾经遇到一个客户提供的MP4,里面视频轨道的 avcC box里SPS写的是1080p,但实际视频数据是720p。提取裸流后,解码器按照SPS的1080p去分配缓冲区,结果解码失败。这种情况,你得用 -bsf:v filter_units=remove_types=... 手动处理,或者干脆重新编码。

4.5 进阶:提取特定NAL单元类型

有时候,你只想提取I帧,或者只想提取SPS/PPS。FFmpeg的比特流过滤器可以做到。

提取所有IDR帧(H.264):

ffmpeg -i input.mp4 -c:v copy -bsf:v h264_mp4toannexb -f h264 - | ffmpeg -i - -c:v copy -bsf:v 'filter_units=pass_types=5' output_idr.h264

这里 pass_types=5 表示只保留NAL类型为5的单元(IDR帧)。H.265的IDR类型是19和20。

提取SPS/PPS:

ffmpeg -i input.mp4 -c:v copy -bsf:v h264_mp4toannexb -f h264 - | ffmpeg -i - -c:v copy -bsf:v 'filter_units=pass_types=7,8' output_sps_pps.h264

7是SPS,8是PPS。H.265的话,VPS是32,SPS是33,PPS是34。

嗯,这里要提醒一下:管道操作虽然方便,但会丢失时间戳信息。如果你需要保持帧的时序,建议用 -vsync 0 或者写脚本分段处理。

4.6 实战:批量提取与校验

假设你有一个目录,里面全是MP4文件。你想批量提取H.264裸流,并校验提取结果。

写个简单的shell脚本:

#!/bin/bash
for f in *.mp4; do
  # 提取裸流
  ffmpeg -i "$f" -c:v copy -bsf:v h264_mp4toannexb "${f%.mp4}.h264" -y
  
  # 校验:检查是否有起始码
  if hexdump -C "${f%.mp4}.h264" | head -n 1 | grep -q "00000001"; then
    echo "✅ $f -> 提取成功"
  else
    echo "❌ $f -> 提取失败,文件可能为空或格式错误"
  fi
done

这个脚本会检查每个输出文件的前几个字节,看是不是以 00 00 00 01 开头。如果是,基本就对了。

我个人习惯再加一步——用 ffprobe 检查裸流的参数是否和源文件一致:

ffprobe -v error -select_streams v:0 -show_entries stream=codec_name,width,height,r_frame_rate -of default=noprint_wrappers=1 input.mp4
ffprobe -v error -select_streams v:0 -show_entries stream=codec_name,width,height,r_frame_rate -of default=noprint_wrappers=1 output.h264

对比两行输出,宽高、帧率、编码器名称应该完全一致。

4.7 知识体系:视频流提取的核心逻辑

下面这张图,帮你理清整个流程:

视频流提取核心流程 MP4 输入文件 含 moov + mdat 步骤1:流拷贝 -c:v copy(不解码) 步骤2:比特流过滤 h264_mp4toannexb H.264/H.265 裸流 Annex B 格式 保持的原始编码参数 • SPS:分辨率、帧率、档次、级别 • PPS:熵编码模式、分片参数 • VPS(H.265):分层编码信息 • SEI:色彩空间、HDR元数据 ⚠ 校验:用 ffprobe 对比提取前后的 codec_name、width、height、r_frame_rate 确保参数完全一致,否则解码器可能无法识别

这张图把整个流程串起来了。从MP4输入,到流拷贝(不解码),再到比特流过滤(转换格式),最后输出裸流。同时,原始编码参数必须完整保留,这是解码器能正常工作的前提。

4.8 常见问题与排查

最后,分享几个我实际踩过的坑:

  • 输出文件大小为0:检查输入文件是否有视频轨道。用 ffprobe -v error -show_entries stream=codec_type -of default=noprint_wrappers=1 input.mp4 看看有没有 codec_type=video
  • 提取后文件变大:正常。因为MP4里NAL单元用4字节长度前缀,而裸流用4字节起始码。长度前缀不占用额外空间(它本身就是数据的一部分),但起始码是额外插入的。如果原始MP4里有很多小NAL单元,起始码的开销会明显。
  • 解码器报错「no frame!」:大概率是SPS/PPS丢失了。检查是否用了 -bsf:v 过滤器。如果用了还不行,试试手动指定 -bsf:v 'h264_mp4toannexb|filter_units=pass_types=7,8,5' 强制保留关键参数。
  • 提取的裸流无法播放:有些播放器要求裸流文件有正确的扩展名。H.264用 .h264.264,H.265用 .h265.hevc。另外,检查一下播放器是否支持裸流——VLC和MPV通常可以,但Windows Media Player不行。

好了,这一讲就到这里。记住核心命令和那个过滤器,大部分场景都能搞定。下一讲我们会聊音频流提取,到时候再细说。


公众号:蓝海资料掘金营,微信deep3321