记录 开发中AVPacket的常见处理

本文介绍了FFmpeg中的AVPacket结构在音视频处理中的作用,包括从原始数据编码到封装成MP4的过程。重点讨论了H264编码格式,特别是帧的分隔,如SPS、PPS和IDR帧的处理。此外,还涉及了解封装后的AVPacket数据格式,以及如何进行帧的分离和复制。在流媒体传输中,正确处理这些帧对于确保解码成功至关重要。
摘要由CSDN通过智能技术生成

说明

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;
    }
}
在将AAC数据包转换为AVPacket结构体时,可以通过计算音频数据的采样率和数据长度来估算音频帧的时长,从而设置AVPacket结构体的duration字段。 具体地,可以按照以下步骤来处理: 1. 获取音频采样率:从AAC数据包获取音频采样率。 2. 计算音频帧时长:根据音频采样率和数据长度,计算出音频帧的时长,单位为秒。 3. 将时长转换为时间基:将音频帧时长转换为AVRational类型的时间基,用于设置AVPacket结构体的duration字段。 4. 设置AVPacket结构体的duration字段:将计算出的音频帧时长(转换为时间基后)设置为AVPacket结构体的duration字段。 下面是一个示例代码,用于将AAC数据包转换为AVPacket结构体,并设置duration字段: ``` // 创建AVPacket结构体 AVPacket *pkt = av_packet_alloc(); // 填充AVPacket结构体 pkt->data = aac_data; // AAC数据包的指针 pkt->size = aac_size; // AAC数据包的大小 pkt->pts = pts; // 时间戳 pkt->stream_index = stream_index; // 流索引 // 估算音频帧时长 int sample_rate = 44100; // 假设音频采样率为44100 double frame_duration = (double)aac_size / (2 * sample_rate); // 计算音频帧时长,单位为秒 // 将时长转换为时间基 AVRational time_base = {1, AV_TIME_BASE}; int64_t duration = (int64_t)(frame_duration * AV_TIME_BASE); pkt->duration = av_rescale_q(duration, time_base, stream_time_base); // stream_time_base为流的时间基 // 复制数据到AVPacket结构体 av_packet_from_data(pkt, pkt->data, pkt->size); // 释放AAC数据包内存 free(aac_data); ``` 需要注意的是,这里只是一个简单的示例代码,实际应用需要根据音频数据的具体情况进行调整。
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

你是周小哥啊

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值