使用ffmpeg读取视频流和其中的SEI数据(未完待续)
FFmpeg是一个多媒体软件框架,支持多种新旧视频编码格式,提供解码、编码、 转码、多路复用、解复用、流式传输、过滤和播放等功能。其包含:
- C++库
libavcodec、libavutil、libavformat、libavfilter、libavdevice、 libswscale
和libswresample
- 基于库构建的命令行工具
ffmpeg
\ffplayer
\ffprobe
关于ffmpeg库:
- libavutil库包含了一系列工具函数用于简化编程,这些工具包括随机数生成器、数据结构、数学例程、核心多媒体实用程序等等。
- libavcodec库包含音频/视频编解码器。
- libavformat用于多媒体容器格式的复用与解复用功能。
- libavdevice是一个包含输入和输出设备的库,用于从设备抓取和渲染到设备。支持许多常见的多媒体输入/输出软件框架,包括Video4Linux、Video4Linux2、VfW和ALSA。
- libavfilter是一个包含媒体过滤器的库。
- libswscale是一个执行高度优化的图像缩放和颜色空间/像素格式转换操作的库。
- libswresample是一个执行高度优化的音频重采样、重匹配和样本格式转换操作的库。
安装ffmpeg c++库
推荐使用vcpkg编译、安装:
vcpkg install ffmpeg:x64-windows
使用ffmpeg读取视频流的示例代码
extern "C"
{
#include <libavcodec/avcodec.h>//avcodec:编解码(最重要的库)
#include <libavformat/avformat.h>//avformat:封装格式处理
#include <libswscale/swscale.h>//swscale:视频像素数据格式转换
#include <libavdevice/avdevice.h>//avdevice:各种设备的输入输出
#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 (frameIndex % 64 == 1/*&& pFrame->key_frame*/)// 每64帧保留一帧
{
std::string strInfo;
//extract_sei_info(packet.data, packet.size, strInfo);
//cout<<strInfo;
if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF)
break;
else if (ret < 0) {
std::cerr << "Error during decoding" << std::endl;
return -1;
}
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;
}
其中,SaveFrameAsJpg函数用于将一帧图像数据保存为jpg格式。
使用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数据的两个函数:
// 从数据流找到包含SEI数据的NALU,然后调用parse_sei_data解析NALU
void extract_sei_info(const uint8_t* data, int size, std::string& sei_geom_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_geom_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;
}
}
}