说明
AVPacket是FFmpeg中极为重要的一种结构,存储着编码后的音视频数据。音视频部分很大一部分逻辑都是围绕着AVPacket做文章进行各种处理。这里记录下常见操作,方便直接复制使用。
基本概念
这里以视频为主介绍,音频可类比。举例MP4文件,生成流程如下:
原始图像数据(yuv) —> 编码数据(h264裸流) —> MP4文件(封装)
对于FFmpeg,对应的大概流程则是:
原始图像数据yuv对应AVFrame,编码数据对应AVPacket, 写入MP4封装头则变成mp4文件
由上可知,AVPacket可谓承上启下。
单帧数据格式
编码数据格式以最为广泛的H264为例
标准格式:起始码startcode+实际数据 起始码作为不同帧数据的分隔,为00 00 00 01或00 00 01
对于FFmpeg中的AVPacket,在实际测试中发现存在两种格式:
1.对于由原始数据编码得到的AVPacket格式与标准格式相同,即起始码startcode+实际数据
2.对于解封装得到的AVPacket格式为四字节长度+实际数据 用前四个字节表示实际数据长度
帧的分隔
视频还有很重要的一点是sps pps idr帧。很多时候如上面的解封装或编码后的AVPacket,不管单独帧的数据格式如何,一个AVPacket里面可能sps+pps+idr三帧包含在一起的。而很多场景下我们需要分离开来,因此要进行帧的分隔,独立sps, pps, idr也方便后续的的处理。
要注意的是对于sps pps:
有的出现一次保存在码器参数的extradata中,多用于本地;
有的会放置于每一个idr帧之前,多用于传输;
编码时如果设置AV_CODEC_FLAG_GLOBAL_HEADER,那么idr前面就不会有sps pps,而是设置到extradata。如果做流媒体传输则不要进行此设置避免对端解码失败。
解析extradata得到sps pps的简单代码,最好参考ffmpeg源码处理更完善
void Worker::parseSPS_PPS(AVCodecContext *videoCtx)
{
//可参考 https://blog.csdn.net/heng615975867/article/details/120026185
if(!videoCtx)
return;
//sps pps保存在解码器上下文的extradata字段 一般都是各1个 这里按照一个解析
char *extraData = (char *)videoCtx->extradata;
int curLen = 6;
spsLen = hexArrayToDec(extraData + curLen, 2);
curLen += 2;
spsFrame = new char[spsLen];
memcpy(spsFrame, extraData + curLen, spsLen);
curLen += spsLen + 1;
ppsLen = hexArrayToDec(extraData + curLen, 2);
curLen += 2;
ppsFrame = new char[ppsLen];
memcpy(ppsFrame, extraData + curLen, ppsLen);
}
其它但重要
开发中会有复制AVPacket数据的场景,需按照下方代码编写,自带接口av_packet_move_ref也可。
void Worker::sendPacketToRtpSender(AVPacket *packet, AVPacketType type)
{
if(packet == nullptr)
return;
//务必av_malloc申请 防止因不配对导致释放错误
int size = packet->size;
char *data = (char *)av_malloc(size);
memcpy(data, packet->data, size);
AVPacket *newPacket = av_packet_alloc();
//做引用计数自动释放data 如果直接data赋值若无手动释放必然内存泄漏
av_packet_from_data(newPacket, (uint8_t *)data, size);
ReadyPacket *readyPacket = new ReadyPacket();
readyPacket->packet = newPacket;
readyPacket->type = type;
rtpSender->writePacket(readyPacket);
}
还有一种经典场景就是三方SDK库会回调出数据,一般是封装为AVPacket然后持续丢到设置好参数的解码器解码。
//必须先拷贝保存一份回调数据 务必av_malloc申请 防止因不配对导致自动释放错误
uint8_t *data = (uint8_t *)av_malloc(stream_len);
memcpy(data, stream_data, stream_len);
//sps、pps或是实际数据帧 AVPacket数据形式必须为startcode + data格式
AVPacket *packet = av_packet_alloc();
packet->pts = pts;
packet->dts = dts;
av_packet_from_data(packet, data, stream_len);
Worker *temp = (Worker *)user_param;
temp->vDecoder->writePacket(packet);
//sps pps 添加起始码示例
int spsSize = sps.size() + 4;
uint8_t *spsData = (uint8_t *)av_malloc(spsSize);
memset(spsData, 0, spsSize);
spsData[3] = 1;
memcpy(spsData + 4, sps.c_str(), sps.size());
AVPacket *spsPkt = av_packet_alloc();
spsPkt->pts = 0;
spsPkt->dts = 0;
av_packet_from_data(spsPkt, spsData, spsSize);
int ppsSize = pps.size() + 4;
uint8_t *ppsData = (uint8_t *)av_malloc(ppsSize);
memset(ppsData, 0, ppsSize);
ppsData[3] = 1;
memcpy(ppsData + 4, pps.c_str(), pps.size());
AVPacket *ppsPkt = av_packet_alloc();
ppsPkt->pts = 0;
ppsPkt->dts = 0;
av_packet_from_data(ppsPkt, ppsData, ppsSize);
具体代码
有了上面的介绍,理解原理后,下面的代码就是对不同场景进行的不同处理。
流过滤
四字节长度+实际数据 转换为 startcode+实际数据
AVBSFContext *bsf_ctx = nullptr;
const AVBitStreamFilter *pfilter = av_bsf_get_by_name("h264_mp4toannexb");
av_bsf_alloc(pfilter, &bsf_ctx);
avcodec_parameters_copy(bsf_ctx->par_in, videoStream->codecpar);
av_bsf_init(bsf_ctx);
//读取packet AVPacket数据形式:4字节长度描述+实际数据
av_read_frame(iFmtCtx, packet);
if (av_bsf_send_packet(bsf_ctx, packet) < 0)
{
av_packet_unref(packet);
continue;
}
while (av_bsf_receive_packet(bsf_ctx, packet) == 0)
{
//AVPacket已转换:startcode+实际数据
fwrite(packet->data, 1, packet->size, file);
av_packet_unref(packet);
}
av_bsf_free(&bsf_ctx);
分离单独帧 起始码+实际数据
按照起始码分隔帧数据
int32_t hexArrayToDec(char *array, int len)
{
if(array == nullptr)
return -1;
int32_t result = 0;
for(int i=0; i<len; i++)
result = result * 256 + (unsigned char)array[i];
return result;
}
int assertStartCode3(char *buf)
{
if (buf[0] == 0 && buf[1] == 0 && buf[2] == 1)
return 1;
else
return 0;
}
int assertStartCode4(char *buf)
{
if (buf[0] == 0 && buf[1] == 0 && buf[2] == 0 && buf[3] == 1)
return 1;
else
return 0;
}
int splitOneH264Frame(char *inFrame, int inSize, int *outSize)
{
//实际数据位置
int pos = 0;
//前缀变量
int isStartCode3 = 0;
int isStartCode4 = 0;
int curStartCodeLen = 0;
//判断前缀长度
isStartCode3 = assertStartCode3(inFrame);
if (isStartCode3 != 1)
{
//如果不是,再读一个字节
isStartCode4 = assertStartCode4(inFrame);
if (isStartCode4 != 1)
{
return -1;
}
else
{
pos = 4;
curStartCodeLen = 4;
}
}
else
{
pos = 3;
curStartCodeLen = 3;
}
//查找到下一个开始前缀的标志位
int nextStartCodeFound = 0;
int nextStartCodeLen = 0;
while (!nextStartCodeFound)
{
if (pos == (inSize - 1)) //判断是否到了数据尾,则返回非0值,否则返回0
{
*outSize = pos + 1;
return curStartCodeLen;
}
pos += 1;
isStartCode4 = assertStartCode4(&inFrame[pos - 4]);//判断是否为0x00000001
if (isStartCode4 != 1)
isStartCode3 = assertStartCode3(&inFrame[pos - 3]);//判断是否为0x000001
nextStartCodeFound = (isStartCode4 == 1 || isStartCode3 == 1);
}
//把pos指向前一个NALU的末尾,在pos位置上偏移下一个前缀长度的值
nextStartCodeLen = (isStartCode4 == 1) ? 4 : 3;
pos -= nextStartCodeLen;
//outSize含有前缀的NALU的长度 curStartCodeLen此数据帧的前缀长度
*outSize = pos;
return curStartCodeLen;
}
/*********************调用示例*********************/
void RtpPacketSender::parseH264PacketDataWithStartCode(RtpPacket *rtpPacket, char *packetData, int packetSize)
{
if(rtpPacket == nullptr || packetData == nullptr || packetSize < 4)
return;
//已解析长度
int hasParseLen = 0;
bool hasSPSPPS = false;
//编码后的AVpacket中存在多帧组合的情况如SPS、PPS、IDR 组包RTP需分开每一帧;部分也有缺少SPS、PPS,需填充
while(1)
{
//编码的AVpacket数据格式:startcode+实际数据
char *curPacketData = packetData + hasParseLen;
int curLen = 0;
int startCodeLen = splitOneH264Frame(curPacketData, packetSize-hasParseLen, &curLen);
hasParseLen += curLen;
char *curFrame = curPacketData + startCodeLen;
//I帧之前没有sps pps则填充
uint8_t typeValue = curFrame[0] & 0x1F;
if(typeValue == 7 || typeValue == 8)
hasSPSPPS = true;
if(this->spsppsSet && typeValue == 5 && hasSPSPPS == false)
{
sendRtpH264Frame(rtpPacket, spsFrame, spsLen);
sendRtpH264Frame(rtpPacket, ppsFrame, ppsLen);
}
//得到单独一帧组包发送
if(typeValue == 5 || typeValue == 1 || typeValue == 7 || typeValue == 8)
sendRtpH264Frame(rtpPacket, curFrame, curLen - startCodeLen);
if(hasParseLen == packetSize)
break;
}
}
分离单独帧 四字节长度+实际数据
按照四字节长度分隔帧
/*********************调用示例*********************/
void RtpPacketSender::parseH264PacketData(RtpPacket *rtpPacket, char *packetData, int packetSize)
{
if(rtpPacket == nullptr || packetData == nullptr || packetSize < 4)
return;
//已解析长度
int hasParseLen = 0;
bool hasSPSPPS = false;
//解封装后的AVpacket中存在多帧组合的情况如SPS、PPS、IDR 组包RTP需分开每一帧;部分也有缺少SPS、PPS,需填充
while(1)
{
//AVpacket数据格式:四字节大小+实际数据
char *curPacketData = packetData + hasParseLen;
int curLen = hexArrayToDec(curPacketData, 4);
hasParseLen += curLen + 4;
char *curFrame = curPacketData + 4;
//I帧之前没有sps pps则填充
uint8_t typeValue = curFrame[0] & 0x1F;
if(typeValue == 7 || typeValue == 8)
hasSPSPPS = true;
if(this->spsppsSet && typeValue == 5 && hasSPSPPS == false)
{
sendRtpH264Frame(rtpPacket, spsFrame, spsLen);
sendRtpH264Frame(rtpPacket, ppsFrame, ppsLen);
}
//得到单独一帧组包发送
if(typeValue == 5 || typeValue == 1 || typeValue == 7 || typeValue == 8)
sendRtpH264Frame(rtpPacket, curFrame, curLen);
if(hasParseLen == packetSize)
break;
}
}