【音视频开发】7. 使用 FFmpeg7 提取 MP4 中的 H264 视频并封装成 Annex-B 流

使用 FFmpeg7 提取 MP4 中的 H264 视频并封装成 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 流

  • 工具链:VS2022,C++20
  • 依赖1:ffmpeg7.1:avcodec,avformat
  • 依赖2:glog
  • 需求:给定一个 含有 h264 编码音频的 mp4 文件,输出一个 Annex-B 封装格式的 h264 流文件
  • 思路:MP4 中的 H264 流不包括 start code 等,使用 ffmpeg 自带的比特流过滤器修改 NALU 之间的封装格式。
c++ 复制代码
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 buffer to store FFmpeg error string
thread_local static char error_buffer[AV_ERROR_MAX_STRING_SIZE] = {};

/**
 * @brief Convert FFmpeg error code to error string
 * @param error_code FFmpeg error code
 * @return error string
 */
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);
}


/**
 * @brief Extract video stream from input file and save it to output file in Annex B format
 * @param fmt_ctx AVFormatContext
 * @param pkt AVPacket
 * @param ofs Output file stream
 * @return true on success, false on failure
 */
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;

    // find h264 video stream
    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;
    }

    // allocate bitstream filter context
    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;
    }

    // initialize bitstream filter context
    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;
        }

        // send packet to bitstream filter
        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);

        // receive packet from bitstream filter
        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;
}

/**
 * @brief Extract video stream from input file and save it to output file in Annex B format
 * @param input_file Input file path
 * @param output_file Output file path
 */
void ExtractVideoStreamAnnexB(const std::string &input_file, const std::string &output_file) {
    int error_code{};

    // open input_file
    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);

    // open output_file
    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;
    }

    // allocate AVPacket
    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
相关推荐
路漫漫心远20 小时前
音视频学习笔记十三——渲染与滤镜之着色器基础
音视频开发
程序员_Rya2 天前
RTC、直播、点播技术对比|腾讯云/即构/声网如何 选型 2025 版
音视频开发·直播·技术选型·音视频sdk·音视频对比
AJi3 天前
FFmpeg学习(五):音视频数据转换
ffmpeg·音视频开发·视频编码
音视频牛哥4 天前
Android平台GB28181执法记录仪技术方案与实现
音视频开发·视频编码·直播
音视频牛哥5 天前
Python下的毫秒级延迟RTSP|RTMP播放器技术探究和AI视觉算法对接
音视频开发·视频编码·直播
jaywangep9 天前
纯前端:提取视频某一帧显示在页面上
前端·音视频开发
音视频牛哥9 天前
Android平台GB28181接入模块(SmartGBD)技术接入说明
音视频开发·视频编码·直播
音视频牛哥15 天前
DaniuSDK:Pioneering the Future of Live Streaming with Cutting-edge SDK Solutions
音视频开发·视频编码·直播