使用ffmpeg的c++库读取视频流和其中的SEI数据

使用ffmpeg读取视频流和其中的SEI数据(未完待续)

FFmpeg是一个多媒体软件框架,支持多种新旧视频编码格式,提供解码、编码、 转码、多路复用、解复用、流式传输、过滤和播放等功能。其包含:

  1. C++库libavcodec、libavutil、libavformat、libavfilter、libavdevice、 libswscalelibswresample
  2. 基于库构建的命令行工具ffmpeg \ ffplayer \ ffprobe

关于ffmpeg库:

  • libavutil库包含了一系列工具函数用于简化编程,这些工具包括随机数生成器、数据结构、数学例程、核心多媒体实用程序等等。
  • libavcodec库包含音频/视频编解码器。
  • libavformat用于多媒体容器格式的复用与解复用功能。
  • libavdevice是一个包含输入和输出设备的库,用于从设备抓取和渲染到设备。支持许多常见的多媒体输入/输出软件框架,包括Video4Linux、Video4Linux2、VfW和ALSA。
  • libavfilter是一个包含媒体过滤器的库。
  • libswscale是一个执行高度优化的图像缩放和颜色空间/像素格式转换操作的库。
  • libswresample是一个执行高度优化的音频重采样、重匹配和样本格式转换操作的库。

安装ffmpeg c++库

推荐使用vcpkg编译、安装:

cmd 复制代码
vcpkg install ffmpeg:x64-windows

使用ffmpeg读取视频流的示例代码

cpp 复制代码
extern "C"
{
#include <libavcodec/avcodec.h>//avcodec:编解码(最重要的库)
#include <libavformat/avformat.h>//avformat:封装格式处理
#include <libswscale/swscale.h>//swscale:视频像素数据格式转换
#include <libavutil/avutil.h>//avutil:工具库(大部分库都需要这个库的支持)
#include <libavutil/imgutils.h>
}
#include<jpeglib.h>
#include<iostream>
#include<fstream>
#include<string>

// Function to save a frame as a JPG image
void SaveFrameAsJpg(const std::string& strDir, AVFrame* pFrameRGB, int width, int height, int frameIndex) {
    char fileName[1024];
    snprintf(fileName, sizeof(fileName), "frame%d.jpg", frameIndex);

    FILE* outFile = fopen((strDir+fileName).c_str(), "wb");
    if (!outFile) {
        std::cerr << "Could not open file for writing: " << fileName << std::endl;
        return;
    }

    struct jpeg_compress_struct cinfo;
    struct jpeg_error_mgr jerr;

    cinfo.err = jpeg_std_error(&jerr);
    jpeg_create_compress(&cinfo);
    jpeg_stdio_dest(&cinfo, outFile);

    cinfo.image_width = width;
    cinfo.image_height = height;
    cinfo.input_components = 3;  // RGB has 3 components
    cinfo.in_color_space = JCS_RGB;

    jpeg_set_defaults(&cinfo);
    jpeg_set_quality(&cinfo, 100, TRUE);
    jpeg_start_compress(&cinfo, TRUE);

    JSAMPROW row_pointer[1];
    while (cinfo.next_scanline < cinfo.image_height) {
        row_pointer[0] = &pFrameRGB->data[0][cinfo.next_scanline * pFrameRGB->linesize[0]];
        jpeg_write_scanlines(&cinfo, row_pointer, 1);
    }

    jpeg_finish_compress(&cinfo);
    jpeg_destroy_compress(&cinfo);
    fclose(outFile);
}

int main()
{
    std::string strStreamAddress="Your video stream address.";
    AVFormatContext* pFormatContext = avformat_alloc_context();
    if (!pFormatContext) {
        std::cerr << "Could not allocate memory for Format Context" << std::endl;
        return -1;
    }
    int ret = avformat_open_input(&pFormatContext, strStreamAddress.c_str(), nullptr, nullptr);
    if (ret != 0)
    {//获取异常信息        
        char* error_info = new char[32];
        av_strerror(ret, error_info, 1024);
        std::cout<<"异常信息 "<<ret<<" "<<error_info << std::endl;
        delete[] error_info;
        return -1;
    }
    std::cout << "打开的视频文件格式:" << pFormatContext->iformat->name<<std::endl;

    std::cout << "查找视频流信息..." << std::endl;
    //参数一:封装格式上下文->AVFormatContext
    //参数二:配置
    //返回值:0>=返回OK,否则失败
    ret = avformat_find_stream_info(pFormatContext, NULL);
    if (ret < 0)
    {//获取异常信息        
        char* error_info = new char[32];
        av_strerror(ret, error_info, 1024);
        std::cout<<"异常信息 "<<error_info << std::endl;
    }

    std::cout << "查找解码器...\n" << "流信息" << pFormatContext->nb_streams << std::endl;
    //第一点:获取当前解码器是属于什么类型解码器->找到了视频流
    //音频解码器、视频解码器、字幕解码器等等...
    //获取视频解码器流引用
    int av_stream_index = -1;
    for (int i = 0; i < pFormatContext->nb_streams; ++i)
    {//循环遍历每个流,例如视频流、音频流、字幕流等等...
        if (pFormatContext->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO)
         {//找到视频流            
            av_stream_index = i;
            break;
        }
    }
    if (av_stream_index == -1)  
        std::cout<<"没有找到视频流";

    AVCodecParameters* pCodecParameters = pFormatContext->streams[av_stream_index]->codecpar;
    const AVCodec* pCodec = avcodec_find_decoder(pCodecParameters->codec_id);
    if (!pCodec) {
        std::cerr << "Unsupported codec!" << std::endl;
        return -1;
    }

    AVCodecContext* pCodecContext = avcodec_alloc_context3(pCodec);
    if (!pCodecContext) {
        std::cerr << "Could not allocate memory for Codec Context" << std::endl;
        return -1;
    }

    if (avcodec_parameters_to_context(pCodecContext, pCodecParameters) < 0) {
        std::cerr << "Could not copy codec parameters to codec context" << std::endl;
        return -1;
    }

    if (avcodec_open2(pCodecContext, pCodec, nullptr) < 0) {
        std::cerr << "Could not open codec" << std::endl;
        return -1;
    }

    AVFrame* pFrame = av_frame_alloc();
    AVFrame* pFrameRGB = av_frame_alloc();
    if (!pFrame || !pFrameRGB) {
        std::cerr << "Could not allocate frame" << std::endl;
        return -1;
    }

    int numBytes = av_image_get_buffer_size(AV_PIX_FMT_RGB24, pCodecContext->width, pCodecContext->height, 1);
    uint8_t* buffer = (uint8_t*)av_malloc(numBytes * sizeof(uint8_t));
    av_image_fill_arrays(pFrameRGB->data, pFrameRGB->linesize, buffer, AV_PIX_FMT_RGB24, pCodecContext->width, pCodecContext->height, 1);

    struct SwsContext* sws_ctx = sws_getContext(
        pCodecContext->width,
        pCodecContext->height,
        pCodecContext->pix_fmt,
        pCodecContext->width,
        pCodecContext->height,
        AV_PIX_FMT_RGB24, SWS_BILINEAR,
        nullptr, nullptr, nullptr
    );

    AVCodecParserContext* parser = av_parser_init(pCodec->id);
    if (!parser) 
    {
        std::cerr << "Could not initialize parser" << std::endl;
        return -1;
    }
    AVPacket packet;
    int frameIndex = 0;
    while (av_read_frame(pFormatContext, &packet) >= 0){
        if (packet.stream_index == av_stream_index) {

            ret = avcodec_send_packet(pCodecContext, &packet);
            if (ret < 0) {
                std::cerr << "Error sending packet for decoding" << std::endl;
                continue;
            }
            while (ret >= 0) {
                ret = avcodec_receive_frame(pCodecContext, pFrame);
                if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) 
                    break;
                else if (ret < 0) {
                    std::cerr << "Error during decoding" << std::endl;
                    return -1;
                }
                if (frameIndex % 64 == 1)// 每64帧保留一帧
                {
                    std::string strInfo;
                    extract_sei_info(packet.data, packet.size, strInfo);
                    cout<<strInfo;
                    sws_scale(
                        sws_ctx,
                        (uint8_t const* const*)pFrame->data,
                        pFrame->linesize,
                        0,
                        pCodecContext->height,
                        pFrameRGB->data,
                        pFrameRGB->linesize
                    );
                    SaveFrameAsJpg(strOutputDir, pFrameRGB, pCodecContext->width, pCodecContext->height, frameIndex);
                }
                frameIndex++;
            }
        }
        av_packet_unref(&packet);
    }
    av_free(buffer);
    av_frame_free(&pFrame);
    av_frame_free(&pFrameRGB);
    avcodec_free_context(&pCodecContext);
    avformat_close_input(&pFormatContext);
    return 0;
}

上述代码中,ffmpeg库函数的作用如下:

  1. 查找视频流并初始化解码器:
    avformat_find_stream_info用于获取文件的流信息。

    在多个流中查找视频流,通过pFormatContext->streams[i]->codecpar->codec_type判断流的类型。
    avcodec_find_decoder查找解码器,并使用avcodec_alloc_context3为解码器分配上下文。

    通过avcodec_parameters_to_context将解码参数拷贝到解码器上下文中,并使用avcodec_open2打开解码器。

  2. 帧的分配和转换:

    使用av_frame_alloc分配原始帧pFrame和RGB格式的帧pFrameRGB
    av_image_get_buffer_sizeav_image_fill_arrays分配和初始化用于存储RGB帧数据的缓冲区。

    使用sws_getContext创建图像转换上下文,配置输入输出的宽度、高度、像素格式等。

  3. 读取帧并解码:

    代码循环读取每个视频包(av_read_frame),并通过avcodec_send_packet将其发送到解码器。
    avcodec_receive_frame接收解码后的帧,并将其通过SaveFrameAsJpg函数保存为JPG图片。

其中,SaveFrameAsJpg函数用于将一帧图像数据保存为jpg格式,extract_sei_info函数用于提取SEI信息到strInfo变量中。extract_sei_info的定义见下一节的代码块。实际上,提取SEI信息还有更简单的方式,可以将extract_sei_info(......);一行替换为strInfo = (char*)(pFrame->side_data[0]->data);。也就是说,解析后的pFrame中的side_data中存储了SEI信息,不需要用户再手动解析比特流中。本文只是为了说明SEI的解析方法,才给出了下一节的分析和代码示例。

使用ffmpeg读取视频流中的SEI数据

SEI是什么

SEI是Supplementary Enhanced Information的简写,意为补充增强信息,是视频流中的可选附加信息,常用于在视频流中插入字幕、时间戳等额外信息。解码视频流的同时将SEI信息解码出来,可以实现多种同步的应用。

SEI在数据流中如何传输?

H.264标准中,音视频流数据必须以NALU(Network Abstraction Layer Unit)的形式传输,SEI数据也不例外,必须被包装在NALU中。顾名思义,NALU是网络抽象层数据传输的单元,其结构为:NALU头+ RBSP

RBSP(Raw Byte Sequence Payload)是原始字节序列负荷,也即数据内容。

在一段NALU之前,必以0x000001或者0x00000001作起始码,紧跟着的8bit便是一个NALU 头。

在H.264标准中,NALU头的8个bit的含义如下:

|0|1|2|3|4|5|6|7|

|F|NRI| Type |

其中,后5个bit表示当前NALU所包含数据的类型,若后5bit的值为0x06,则说明该数据段为SEI信息单元;若值为0x05,则说明该数据段为视频的IDR帧......

总之,软件根据Type信息知晓当前NALU包含的是什么数据,从而选择对应的数据解析方式。

读取SEI的示例代码

根据上面的介绍,要读取SEI数据,首先需要解析出Type为6的NALU单元。然后,读取

SEI中写入的信息是用户自定义的,根据视频流数据发布者提供的信息,只要找到SEI数据开始的位置,即可解析。

下面给出解析SEI数据的两个函数:

cpp 复制代码
// 从数据流找到包含SEI数据的NALU,然后调用parse_sei_data解析NALU
void extract_sei_info(const uint8_t* data, int size, std::string& sei_info) {
    // Check if the packet is an H.264 NAL unit
    if (data && size > 0) {
        int i = 0;
        unsigned char* pstr = nullptr;
        int nLen = 0;
        while (i < size - 4) {
            uint8_t nal_type = 0;
            if (data[i] == 0x00 && data[i + 1] == 0x00 && data[i + 2] == 0x00 && data[i + 3] == 0x01) {
            nal_type = data[i + 4] & 0x1F;
            if (nal_type == 6 && (data[i + 5] & 0x1F) == 5) {
                std::cout << "Found SEI NAL unit" << std::endl;
                parse_sei_data(data + i + 5, size - (i + 5), &pstr, nLen);
                break;
            }
        }
        else if (data[i] == 0x00 && data[i + 1] == 0x00 && data[i + 2] == 0x01) {
            nal_type = data[i + 3] & 0x1F;
            if (nal_type == 6 && (data[i + 5] & 0x1F) == 5) {
                std::cout << "Found SEI NAL unit" << std::endl;
                parse_sei_data(data + i + 4, size - (i + 4), &pstr, nLen);
                break;
            }
        }
        else
            i++;
        }
        if(pstr!=nullptr)sei_info=pstr;
    }
    return;
}


// 提取SEI中的数据(数据指针*pParsedData、数据大小nSize)
void parse_sei_data(const uint8_t* data, int size, uint8_t** pParsedData, int& nSize) {
    // Simplified SEI parsing logic
    int offset = 0;
    while (offset < size) {
        uint8_t payload_type = 0;
        uint8_t payload_size = 0;

        // Read payload type
        while (offset < size && data[offset] == 0xFF) {
            payload_type += 255;
            offset++;
        }
        if (offset < size) {
            payload_type += data[offset++];
        }

        // Read payload size
        while (offset < size && data[offset] == 0xFF) {
            payload_size += 255;
            offset++;
        }
        if (offset < size) {
            payload_size += data[offset++];
        }

        // Payload data
        if (offset + payload_size > size) {
            std::cerr << "Invalid SEI payload size" << std::endl;
            return;
        }

        uint8_t* payload_data = (uint8_t*)data + offset;
        offset += payload_size;

        // Here you can parse the specific SEI payload data based on payload_type
        std::cout << "SEI message: type=" << (int)payload_type << ", size=" << (int)payload_size << std::endl;
        if (payload_type == 5)
        {
            pParsedData[0] = payload_data;
            nSize = payload_size;
            break;
        }
    }
}

这两个函数 extract_sei_infoparse_sei_data 用于从视频数据流中提取并解析 SEI(Supplemental Enhancement Information,补充增强信息)消息。SEI 是在视频编码中用于传输非视频数据的信息,比如字幕、版权信息、或其他元数据。

1. extract_sei_info 函数

作用:

  • 从 H.264/H.265 数据流中找到包含 SEI 数据的 NALU(Network Abstraction Layer Unit)单元
  • 调用 parse_sei_data 函数来解析找到的 SEI 数据 ,并将解析结果存储到 sei_info 字符串中。

工作流程:

  1. 检查数据包是否为 H.264/H.265 NAL 单元

    • 通过检查数据的起始码 0x00 0x00 0x00 0x010x00 0x00 0x01,判断当前数据是否为一个 NAL 单元。
  2. 识别 NAL 单元类型

    • 使用 NAL 单元头中的信息识别 NAL 单元的类型,特别是 SEI 类型(nal_type == 6 表示 SEI 单元)。
  3. 解析 SEI 数据

    • 如果找到 SEI 单元,调用 parse_sei_data 函数来解析 SEI 数据,并将结果存储到 sei_info 中。
  4. 终止循环

    • 一旦找到并解析了 SEI 数据,循环终止。

代码中的细节:

  • nal_type == 6 判断当前 NAL 单元是否为 SEI。
  • (data[i + 5] & 0x1F) == 5 进一步确认 SEI 消息的具体类型(5表示用户自定义类型,可能表示这是一种特定的 SEI 消息类型)。
2. parse_sei_data 函数

作用:

  • 解析 SEI 数据,提取特定的 SEI 消息 ,特别是 payload_type 为 5 的 SEI 消息。

工作流程:

  1. 读取 SEI 消息的 payload_typepayload_size

    • payload_type:SEI 消息的类型,由 SEI 数据头部的前几字节决定。它可能会被编码为多个 0xFF 字节(255),因此需要累加。
    • payload_size:SEI 消息的大小,表示这个 SEI 消息的实际数据长度。
  2. 提取 SEI 消息的负载数据

    • 在解析出 payload_typepayload_size 后,提取对应的 SEI 数据。
    • 对于特定的 payload_type(在此例中为 5),将 SEI 数据存储在 pParsedData 中,并设置数据大小 nSize
  3. 输出 SEI 消息类型和大小

    • 每次解析到一个 SEI 消息时,都会输出其类型和大小。
  4. 终止循环

    • 当找到指定的 SEI 消息类型(payload_type == 5)时,终止解析。

代码中的细节:

  • payload_typepayload_size 的解析使用了多个 0xFF 字节累加的方式,这是一种处理 H.264/HEVC SEI 的标准方法。
  • 解析完成后,通过 pParsedData[0] 指向 SEI 的实际数据,并设置 nSize 为 SEI 数据的大小。
相关推荐
小芒果_011 分钟前
P11229 [CSP-J 2024] 小木棍
c++·算法·信息学奥赛
gkdpjj7 分钟前
C++优选算法十 哈希表
c++·算法·散列表
王俊山IT9 分钟前
C++学习笔记----10、模块、头文件及各种主题(一)---- 模块(5)
开发语言·c++·笔记·学习
为将者,自当识天晓地。11 分钟前
c++多线程
java·开发语言
-Even-11 分钟前
【第六章】分支语句和逻辑运算符
c++·c++ primer plus
小政爱学习!13 分钟前
封装axios、环境变量、api解耦、解决跨域、全局组件注入
开发语言·前端·javascript
k093328 分钟前
sourceTree回滚版本到某次提交
开发语言·前端·javascript
神奇夜光杯36 分钟前
Python酷库之旅-第三方库Pandas(202)
开发语言·人工智能·python·excel·pandas·标准库及第三方库·学习与成长
Themberfue38 分钟前
Java多线程详解⑤(全程干货!!!)线程安全问题 || 锁 || synchronized
java·开发语言·线程·多线程·synchronized·
plmm烟酒僧40 分钟前
Windows下QT调用MinGW编译的OpenCV
开发语言·windows·qt·opencv