使用 FFmpeg API 封装 Annex-B 视频流
1、NALU
- VCL 视频编码层:对输入的视频图像序列进行高效的编码压缩,将视频的原始像素数据转换为适合在网络上传输或存储的编码数据形式
- NAL 网络抽象层:将 VCL 产生的视频数据进行封装和抽象,使其能够适应不同的网络传输和存储环境。
- NALU 网络抽象层单元:H.264/H.265 等视频编码标准数据在网络传输或存储时的基本单元
- 一个 NALU 的组成:start code + 头部 + 负载
- start code:不一定有,"00 00 00 01" 或 "00 00 01"
- 注意:ffmpeg 解复用 MP4 容器后的
AVPacket
通常没有 start code,TS 容器一般有
2、Annex-B 格式
- AVCC/AVC1:常见的 MP4 容器使用,每个 NALU 以其长度开头,SPS 和 PPS 等信息在 MP4 容器中
- Annex-B:常见的流容器使用,每个 NALU 都以特定的 start code 开头,SPS 和 PPS 等信息在流头部
3、代码实战 —— 从 mp4 中提取 Annex-B 流
- 需求:给定一个 含有 h264 编码音频的 mp4 文件,输出一个 Annex-B 封装格式的 h264 流文件
- 思路:MP4 中的 H264 流不包括 start code 等,使用 ffmpeg 自带的比特流过滤器修改 NALU 之间的封装格式。
- 代码示例的环境:
- 工具链:VS2022,C++20
- 依赖1:ffmpeg7.1 的 avcodec,avformat,avutil
- 依赖2:glog
extern "C" {
#include <libavcodec/bsf.h>
#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
}
#include <glog/logging.h>
#include <cstdio>
#include <string>
#include <fstream>
thread_local static char error_buffer[AV_ERROR_MAX_STRING_SIZE] = {};
static char *ErrorToString(const int error_code) {
std::memset(error_buffer, 0, AV_ERROR_MAX_STRING_SIZE);
return av_make_error_string(error_buffer, AV_ERROR_MAX_STRING_SIZE, error_code);
}
static bool InnerExtractVideoStreamAnnexB(AVFormatContext *fmt_ctx, AVPacket *pkt, std::ofstream &ofs) {
if (!fmt_ctx || !pkt || !ofs) {
return false;
}
int error_code{};
int h264_stream_index = -1;
AVStream *stream = nullptr;
AVCodecParameters *codec_params = nullptr;
for (int i = 0; i < fmt_ctx->nb_streams; i++) {
stream = fmt_ctx->streams[i];
codec_params = stream->codecpar;
if (codec_params->codec_type == AVMEDIA_TYPE_VIDEO) {
if (codec_params->codec_id == AV_CODEC_ID_H264) {
h264_stream_index = i;
break;
}
}
}
if (h264_stream_index < 0) {
LOG(ERROR) << "Could not find H264 video stream";
return false;
}
const AVBitStreamFilter *bs_filter = av_bsf_get_by_name("h264_mp4toannexb");
if (bs_filter == nullptr) {
LOG(ERROR) << "Could not find h264_mp4toannexb bitstream filter";
return false;
}
AVBSFContext *bsf_ctx = nullptr;
if ((error_code = av_bsf_alloc(bs_filter, &bsf_ctx)) < 0) {
LOG(ERROR) << "Could not allocate bitstream filter context: " << ErrorToString(error_code);
return false;
}
if ((error_code = avcodec_parameters_copy(bsf_ctx->par_in, codec_params)) < 0) {
LOG(ERROR) << "Could not copy codec parameters: " << ErrorToString(error_code);
av_bsf_free(&bsf_ctx);
return false;
}
if ((error_code = av_bsf_init(bsf_ctx)) < 0) {
LOG(ERROR) << "Could not initialize bitstream filter context: " << ErrorToString(error_code);
av_bsf_free(&bsf_ctx);
return false;
}
while (true) {
if ((error_code = av_read_frame(fmt_ctx, pkt)) < 0) {
if (error_code != AVERROR_EOF) {
LOG(ERROR) << "Could not read frame: " << ErrorToString(error_code);
} else {
LOG(INFO) << "End of input file";
}
break;
}
if (pkt->stream_index != h264_stream_index) {
av_packet_unref(pkt);
continue;
}
if ((error_code = av_bsf_send_packet(bsf_ctx, pkt)) < 0) {
LOG(ERROR) << "Could not send packet to bitstream filter: " << ErrorToString(error_code);
av_packet_unref(pkt);
continue;
}
av_packet_unref(pkt);
while ((error_code = av_bsf_receive_packet(bsf_ctx, pkt)) == 0) {
if (!ofs.write(reinterpret_cast<const char *>(pkt->data), pkt->size)) {
LOG(ERROR) << "Could not write ha64 file: ofstream is broken";
av_packet_unref(pkt);
return false;
}
LOG(INFO) << "Extracted " << pkt->size << " bytes of H264 data";
av_packet_unref(pkt);
}
if (error_code != AVERROR(EAGAIN)) {
av_packet_unref(pkt);
LOG(ERROR) << "Could not receive packet from bitstream filter: " << ErrorToString(error_code);
}
}
av_bsf_free(&bsf_ctx);
return true;
}
void ExtractVideoStreamAnnexB(const std::string &input_file, const std::string &output_file) {
int error_code{};
AVFormatContext *fmt_ctx = nullptr;
if ((error_code = avformat_open_input(&fmt_ctx, input_file.c_str(), nullptr, nullptr)) < 0) {
LOG(ERROR) << "Could not open source file \"" << input_file << "\": " << ErrorToString(error_code);
return;
}
if ((error_code = avformat_find_stream_info(fmt_ctx, nullptr)) < 0) {
LOG(ERROR) << "Could not find stream information: " << ErrorToString(error_code);
avformat_close_input(&fmt_ctx);
return;
}
av_dump_format(fmt_ctx, 0, input_file.c_str(), 0);
std::ofstream ofs(output_file, std::ios::out | std::ios::binary);
if (!ofs.is_open()) {
LOG(ERROR) << "Could not open output file \"" << output_file << "\"";
avformat_close_input(&fmt_ctx);
return;
}
AVPacket *pkt = nullptr;
if ((pkt = av_packet_alloc()) == nullptr) {
LOG(ERROR) << "Could not allocate AVPacket: av_packet_alloc()";
avformat_close_input(&fmt_ctx);
return;
}
InnerExtractVideoStreamAnnexB(fmt_ctx, pkt, ofs);
ofs.close();
av_packet_free(&pkt);
avformat_close_input(&fmt_ctx);
}
#if 0
int main(int argc, char *argv[]) {
google::InitGoogleLogging(argv[0]);
FLAGS_logtostderr = true;
FLAGS_minloglevel = google::GLOG_INFO;
ExtractVideoStreamAnnexB("input.mp4", "output.h264");
google::ShutdownGoogleLogging();
return 0;
}
#endif