当前位置: 首页 > article >正文

RV1126+FFMPEG推流项目(11)编码音视频数据 + FFMPEG时间戳处理

        本节介绍本章节主要讲解的是push_server_thread线程的具体处理流程, push_server_thread这个线程的主要功能是通过时间戳比较,来处理音频、视频的数据并最终推流到SRT、RTMP、UDP、RTSP服务器

push_server_thread:流程如下

        上图,主要阐述了push_server_thread的工作流程,因为这个线程主要处理的是通过时间戳进行比较(av_compare_ts)。若检测到音频时间戳则处理音频数据,若检测到视频时间戳则处理视频数据,最终把音视频数据合成到TS、FLV并推流到RTMP、SRT、UDP、RTSP服务器。   

        上图, 是视频编码时间戳、音频编码时间戳经过了时间基转换后的具体数值:视频时间基成video_time_base = {1,25},音频时间基audio_time_base = {1,48000}转换成TS后:视频PTS = {0,3600,7200,10800,14400,18000…},音频PTS = {0, 1920,3840,5760,7680,9600…}。

这里要注意的是:

  1.        在这个推流项目中视频帧率和时间基固定成video_time_base = {1,25},video_frame_rate = {1,25}。因为底层驱缘故,易百纳的摄像头帧率可能只支持25帧,所以编码帧率和时间基只能设置{1,25},否则就会导致音视频不同步。
    1.         视频VIDEO_PTS和音频AUDIO_PTS,需要按照一定的数值规律进行累加。中间不能出现任何的丢失和错误,否则就会出现各种问题,如花屏、卡顿、音视频不同步等问题。 比方说:video_pts = {0,3600,7200,,14400}这种属于PTS出现丢失;
    2. push_server_thread线程模块讲解:

// 音视频合成推流线程
/**
 * @brief 推送服务器线程的入口函数
 * 
 * 该函数负责在一个独立的线程中处理音视频数据的推送任务。
 * 它通过比较视频和音频的时间戳来决定下一个要处理的数据类型,
 * 以确保音视频同步。此外,它还负责释放相关的资源。
 * 
 * @param args 传递给线程的参数,这里是FFMPEG的配置信息
 * @return void* 返回线程的退出状态
 */
void *push_server_thread(void *args)
{
    // 确保线程可以独立运行,即使父线程结束,该线程也不会变为僵死状态
    pthread_detach(pthread_self());

    // 将传递给线程的参数转换为所需的结构体类型
    RKMEDIA_FFMPEG_CONFIG ffmpeg_config = *(RKMEDIA_FFMPEG_CONFIG *)args;

    // 释放传递给线程的参数内存
    free(args);

    // 初始化AVOutputFormat指针
    AVOutputFormat *fmt = NULL;

    // 初始化返回值变量
    int ret;

    // 无限循环,处理音视频数据
    while (1)
    {
        /*
         我们以转换到同一时基下的时间戳为例,假设上一时刻音、视频帧的保存时间戳都是0。
         当前任意保存一种视频帧,例如保存视频的时间戳为video_t1。接着比较时间戳,发现音频时间戳为0 < video_t1,保存一帧音频,时间戳为audio_t1。
         继续比较时间戳,发现audio_t1 < video_t1,选择保存一帧音频,时间戳为audio_t2。
         再一次比较时间戳video_t1 < audio_t2,选择保存一帧视频,时间戳为video_t2。
         int av_compare_ts(int64_t ts_a, AVRational_tb_b,int64_t ts_b, AVRational tb_b)
         {
             int64_t a = tb_a.num * (int64_t)tb_b.den;
             int64_t b = tb_b.num * (int64_t)tb_a.den;
             if ((FFABS64U(ts_a)|a|FFABS64U(ts_b)|b) <= INT_MAX)
                 return (ts_a*a > ts_b*b) - (ts_a*a < ts_b*b);
             if (av_rescale_rnd(ts_a, a, b, AV_ROUND_DOWN) < ts_b)
                 return -1;
              if (av_rescale_rnd(ts_b, b, a, AV_ROUND_DOWN) < ts_a)
                 return -1;
             return 0;
         }
         */
        // 比较视频和音频的时间戳,决定下一个要处理的数据类型
        ret = av_compare_ts(ffmpeg_config.video_stream.next_timestamp,
                            ffmpeg_config.video_stream.enc->time_base,
                            ffmpeg_config.audio_stream.next_timestamp,
                            ffmpeg_config.audio_stream.enc->time_base);

        // 如果视频时间戳小于等于音频时间戳,处理视频数据
        if (ret <= 0)
        {
            ret = deal_video_avpacket(ffmpeg_config.oc, &ffmpeg_config.video_stream); // 处理FFMPEG视频数据
            if (ret == -1)
            {
                printf("deal_video_avpacket error\n");
                break;
            }
        }
        else // 否则,处理音频数据
        {
            ret = deal_audio_avpacket(ffmpeg_config.oc, &ffmpeg_config.audio_stream); // 处理FFMPEG音频数据
            if (ret == -1)
            {
                printf("deal_video_avpacket error\n");
                break;
            }
        }
    }

    // 写入AVFormatContext的尾巴
    av_write_trailer(ffmpeg_config.oc);

    // 释放VIDEO_STREAM的资源
    free_stream(ffmpeg_config.oc, &ffmpeg_config.video_stream);

    // 释放AUDIO_STREAM的资源
    free_stream(ffmpeg_config.oc, &ffmpeg_config.audio_stream);

    // 释放AVIO资源
    avio_closep(&ffmpeg_config.oc->pb);

    // 释放AVFormatContext资源
    avformat_free_context(ffmpeg_config.oc);

    return NULL;
}

        上面的代码就是push_server_thread线程的主要工作, 从上面的的代码可以分析到av_compare_ts去进行每一帧时间戳的比较。我们设定用ts_atb_a作为视频的时间戳时间基ts_btb_b作为音频的时间戳时间基。若ret(返回值)<=0,则说明此时要处理视频编码数据,就调用deal_video_avpacket函数进行视频编码数据的写入;否则就调用deal_audio_avpacket进行音频编码数据的写入,当这个线程退出后, 先av_write_trailer结束写入文件结束符,并释放所有的资源数据(free_streamavio_clospavforamt_free_context)。

av_compare_ts的作用:

        把音视频的顺序弄正确,防止解码端解码端出错。它的主要作用是进行时间戳进行实时比较,它能够实时保证当前的时间戳是,准确无误的。它不会出现时间戳混乱的情况,所谓混乱的情况就相当于:视频时间戳当成音频时间戳处理,音频时间戳当成视频时间戳处理。

 

       push_server_thread线程,里面最重要的两个函数 deal_video_avpacket和deal_audio_avpacket

deal_video_avpacket:

/**
 * 处理视频AVPacket,将其写入到复合流中
 * 
 * @param oc AVFormatContext指针,表示复合流的上下文
 * @param ost OutputStream指针,包含编码和流信息
 * @return 成功返回0,失败返回-1
 */
int deal_video_avpacket(AVFormatContext *oc, OutputStream *ost)
{
    int ret;
    AVCodecContext *c = ost->enc; // 获取编码器上下文
    AVPacket *video_packet = get_ffmpeg_video_avpacket(ost->packet); // 从RV1126视频编码数据赋值到FFMPEG的Video AVPacket中
    if (video_packet != NULL)
    {
        video_packet->pts = ost->next_timestamp++; // VIDEO_PTS按照帧率进行累加
    }

    ret = write_ffmpeg_avpacket(oc, &c->time_base, ost->stream, video_packet); // 向复合流写入视频数据
    if (ret != 0)
    {
        printf("write video avpacket error");
        return -1;
    }

    return 0;
}

deal_video_avpacket函数里面主要包含了以下重要的功能: 

        第一步:通过get_ffmpeg_video_avpacket函数里面,从视频队列中获取视频编码数据,并把视频数据赋值到AVPacket里面(这里很重要,因为我们最终推流用的都是AVPacket结构体数据)。

get_ffmpeg_video_avpacket:

AVPacket *get_ffmpeg_video_avpacket(AVPacket *pkt)
{
    video_data_packet_t *video_data_packet = video_queue->getVideoPacketQueue(); // 从视频队列获取数据

    if (video_data_packet != NULL)
    {
/*
    重新为FFMPEG的Video AVPacket分配给定的缓冲区
    1.  如果入参的 AVBufferRef 为空,直接调用 av_realloc 分配一个新的缓存区,并调用 av_buffer_create 返回一个新的 AVBufferRef 结构;
    2.  如果入参的缓存区长度和入参 size 相等,直接返回 0;
    3.  如果对应的 AVBuffer 设置了 BUFFER_FLAG_REALLOCATABLE 标志,或者不可写,再或者 AVBufferRef data 字段指向的数据地址和 AVBuffer 的 data 地址不同,
        递归调用 av_buffer_realloc 分配一个新的 buffer,并将 data 拷贝过去;
    4.  不满足上面的条件,直接调用 av_realloc 重新分配缓存区。
 */
        int ret = av_buffer_realloc(&pkt->buf, video_data_packet->video_frame_size + 70);
        if (ret < 0)
        {
            return NULL;
        }
        pkt->size = video_data_packet->video_frame_size;                                        // rv1126的视频长度赋值到AVPacket Size
        memcpy(pkt->buf->data, video_data_packet->buffer, video_data_packet->video_frame_size); // rv1126的视频数据先拷贝到ptk->buf->data中
        pkt->data = pkt->buf->data;                                                             // 把pkt->buf->data赋值到pkt->data,如果直接赋给pkt->data,会报错
        pkt->flags |= AV_PKT_FLAG_KEY;                                                          // 默认flags是AV_PKT_FLAG_KEY,关键帧,如果没有回黑屏
        if (video_data_packet != NULL)
        {
            free(video_data_packet);                                                            //释放掉内存
            video_data_packet = NULL;
        }
        //已经把视频队列里面的数据已经拷贝到了ffmpeg的packet的data中。

        return pkt;     //返回一个指针,指向ffmpeg的packet的data,因为我们最终推流用的都是AVPacket结构体数据
    }
    else
    {
        return NULL;  //队列里面没有数据了,
    }
}

这里需要注意的有两个地方:

      在AVPacket中buf的赋值,不能够直接赋值,如: memcpy(pkt->data, video_data_packet->buffer, video_data_packet->frame_size)否则程序就会出现core_dump情况。我们需要先把video_data_packet_t的视频数据(video_data_packet->buffer)先拷贝到pkt->buf->data,然后再把pkt->buf->data的数据赋值到pkt->data。

  memcpy(pkt->buf->data, video_data_packet->buffer, video_data_packet->video_frame_size); // rv1126的视频数据先拷贝到ptk->buf->data中
  pkt->data = pkt->buf->data;   

        对于视频的AVPacket中,需要对它的标识符flag进行关键帧设置(pkt->flags |= AV_PKT_FLAG_KEY),否则解码端则无法正常播放视频。代码如下:

pkt->flags |= AV_PKT_FLAG_KEY;  // 默认flags是AV_PKT_FLAG_KEY,关键帧,如果没有会没有办法播放,黑屏

        第二步:根据AVPacket的数据去计算视频的PTS,若AVPacket的数据不为空。则让视频video_packet->pts = ost->next_timestamp++; (关于video的PTS计算,上一篇已经聊过了)

        第三步:write_ffmpeg_avpacket:把视频PTS进行时间基的转换,调用av_packet_rescale_ts把采集的视频时间基转换成复合流的时间基。时间基转换完成之后,就把视频数据写入到复合流文件里面,调用的API是av_interleaved_write_frame (注意:复合流文件可以是本地文件也可以是流媒体地址)。

/**
 * 写入FFmpeg视频数据包
 * 
 * 此函数负责将一个AVPacket中的数据写入到视频文件中在写入之前,它会根据提供的time_base和流的time_base调整AVPacket的时间戳
 * 这是为了确保时间戳匹配流的时基,防止播放时出现同步问题
 * 
 * @param fmt_ctx FFmpeg格式上下文,用于写入数据
 * @param time_base 指向AVRational的指针,表示时间基数
 * @param st 视频流,用于确定stream_index
 * @param pkt 包含编码视频数据的AVPacket
 * @return 返回av_interleaved_write_frame的结果,表示写入操作是否成功
 */
int write_ffmpeg_avpacket(AVFormatContext *fmt_ctx, const AVRational *time_base, AVStream *st, AVPacket *pkt)
{
    /*将输出数据包时间戳值从编解码器重新调整为流时基 */
    av_packet_rescale_ts(pkt, *time_base, st->time_base);
    pkt->stream_index = st->index;

    // 向复合流写入视频数据,复合流文件可以是本地文件也可以是流媒体地址
    return av_interleaved_write_frame(fmt_ctx, pkt); 
}

 deal_audio_avpacket的实现:流程和视频的基本一样


int deal_audio_avpacket(AVFormatContext *oc, OutputStream *ost)
{
    int ret;
    AVCodecContext *c = ost->enc;
    AVPacket *audio_packet = get_ffmpeg_audio_avpacket(ost->packet); // 从RV1126视频编码数据赋值到FFMPEG的Audio AVPacket中
    if (audio_packet != NULL)
    {
        audio_packet->pts = ost->samples_count;
        ost->samples_count += 1024;
        ost->next_timestamp = ost->samples_count; // AUDIO_PTS按照帧率进行累加1024
    }

    ret = write_ffmpeg_avpacket(oc, &c->time_base, ost->stream, audio_packet); // 向复合流写入音频数据
    if (ret != 0)
    {
        printf(" write audio avpacket error");
        return -1;
    }

    return 0;
}

 deal_audio_avpacket函数里面主要包含了以下重要的功能: 

        第一步:通过get_ffmpeg_audio_avpacket函数里面,从音频队列中获取音频编码数据,并把音频数据赋值到AVPacket里面(这里很重要,因为我们最终推流用的都是AVPacket结构体数据)。具体的赋值如下图:


AVPacket *get_ffmpeg_audio_avpacket(AVPacket *pkt)
{
    audio_data_packet_t *audio_data_packet = audio_queue->getAudioPacketQueue();// 从音频队列获取数据

    if (audio_data_packet != NULL)
    {
        /*
  重新分配给定的缓冲区
1.  如果入参的 AVBufferRef 为空,直接调用 av_realloc 分配一个新的缓存区,并调用 av_buffer_create 返回一个新的 AVBufferRef 结构;
2.  如果入参的缓存区长度和入参 size 相等,直接返回 0;
3.  如果对应的 AVBuffer 设置了 BUFFER_FLAG_REALLOCATABLE 标志,或者不可写,再或者 AVBufferRef data 字段指向的数据地址和 AVBuffer 的 data 地址不同,递归调用 av_buffer_realloc 分配一个新
的 buffer,并将 data 拷贝过去;
4.  不满足上面的条件,直接调用 av_realloc 重新分配缓存区。
*/
        int ret = av_buffer_realloc(&pkt->buf, audio_data_packet->audio_frame_size + 70);
        if (ret < 0)
        {
            return NULL;
        }

        pkt->size = audio_data_packet->audio_frame_size; // rv1126的音频长度赋值到AVPacket Size
        memcpy(pkt->buf->data, audio_data_packet->buffer, audio_data_packet->audio_frame_size); //rv1126的音频数据赋值到AVPacket data
        pkt->data = pkt->buf->data; // 把pkt->buf->data赋值到pkt->data

        if (audio_data_packet != NULL)
        {
            free(audio_data_packet);
            audio_data_packet = NULL;
        }

        return pkt;
    }
    else
    {
        return NULL;
    }
}

我们来分析音频AVPacket如何赋值:

        第一步:在AVPacket中buf的赋值,不能够直接赋值,如: memcpy(pkt->data, audio_data_packet->buffer, audio_data_packet->frame_size)否则程序就会出现core_dump情况。我们需要先把audio_data_packet_t的视频数据(audio_data_packet->buffer)先拷贝到pkt->buf->data,然后再把pkt->buf->data的数据赋值到pkt->data。

        第二步:根据AVPacket的数据去计算音频的PTS,若音频AVPacket的数据不为空。则对音频PTS进行计算,计算公式如下:

audio_packet->pts = ost->samples_count;

ost->samples_count += 1024;

ost->next_timestamp = ost->samples_count; // AUDIO_PTS按照帧率进行累加1024

(关于audio的PTS计算是每次累加1024,上一节课已经讲了)

第三步:和视频一样把音频PTS进行时间基的转换,调用av_packet_rescale_ts把采集的音频时间基转换成复合流的时间基。时间基转换完成之后,就把音频数据写入到复合流文件里面,调用的API是同样也是av_interleaved_write_frame (注意:复合流文件可以是本地文件也可以是流媒体地址)。

 最后一步释放资源:

void *push_server_thread(void *args)
{
    // 确保线程可以独立运行,即使父线程结束,该线程也不会变为僵死状态
    pthread_detach(pthread_self());

    // 将传递给线程的参数转换为所需的结构体类型
    RKMEDIA_FFMPEG_CONFIG ffmpeg_config = *(RKMEDIA_FFMPEG_CONFIG *)args;

    // 释放传递给线程的参数内存
    free(args);

    // 初始化AVOutputFormat指针
    AVOutputFormat *fmt = NULL;

    // 初始化返回值变量
    int ret;

    // 无限循环,处理音视频数据
    while (1)
    {
        /*
         我们以转换到同一时基下的时间戳为例,假设上一时刻音、视频帧的保存时间戳都是0。
         当前任意保存一种视频帧,例如保存视频的时间戳为video_t1。接着比较时间戳,发现音频时间戳为0 < video_t1,保存一帧音频,时间戳为audio_t1。
         继续比较时间戳,发现audio_t1 < video_t1,选择保存一帧音频,时间戳为audio_t2。
         再一次比较时间戳video_t1 < audio_t2,选择保存一帧视频,时间戳为video_t2。
         int av_compare_ts(int64_t ts_a, AVRational_tb_b,int64_t ts_b, AVRational tb_b)
         {
             int64_t a = tb_a.num * (int64_t)tb_b.den;
             int64_t b = tb_b.num * (int64_t)tb_a.den;
             if ((FFABS64U(ts_a)|a|FFABS64U(ts_b)|b) <= INT_MAX)
                 return (ts_a*a > ts_b*b) - (ts_a*a < ts_b*b);
             if (av_rescale_rnd(ts_a, a, b, AV_ROUND_DOWN) < ts_b)
                 return -1;
              if (av_rescale_rnd(ts_b, b, a, AV_ROUND_DOWN) < ts_a)
                 return -1;
             return 0;
         }
         */
        // 比较视频和音频的时间戳,决定下一个要处理的数据类型
        ret = av_compare_ts(ffmpeg_config.video_stream.next_timestamp,
                            ffmpeg_config.video_stream.enc->time_base,
                            ffmpeg_config.audio_stream.next_timestamp,
                            ffmpeg_config.audio_stream.enc->time_base);

        // 如果视频时间戳小于等于音频时间戳,处理视频数据
        if (ret <= 0)
        {
            ret = deal_video_avpacket(ffmpeg_config.oc, &ffmpeg_config.video_stream); // 处理FFMPEG视频数据
            if (ret == -1)
            {
                printf("deal_video_avpacket error\n");
                break;
            }
        }
        else // 否则,处理音频数据
        {
            ret = deal_audio_avpacket(ffmpeg_config.oc, &ffmpeg_config.audio_stream); // 处理FFMPEG音频数据
            if (ret == -1)
            {
                printf("deal_video_avpacket error\n");
                break;
            }
        }
    }

    // 写入AVFormatContext的尾巴
    av_write_trailer(ffmpeg_config.oc);

    // 释放VIDEO_STREAM的资源
    free_stream(ffmpeg_config.oc, &ffmpeg_config.video_stream);

    // 释放AUDIO_STREAM的资源
    free_stream(ffmpeg_config.oc, &ffmpeg_config.audio_stream);

    // 释放AVIO资源
    avio_closep(&ffmpeg_config.oc->pb);

    // 释放AVFormatContext资源
    avformat_free_context(ffmpeg_config.oc);

    return NULL;
}

avcodec_close关闭编码器

avcodec_free_context释放解码器上下文

av_buffer_unref将当前的AVBufferRef指针指向的内存释放,并对AVBufferRef指向的数据引用计数减1

av_packet_unref对AVPacket进行清理

av_packet_free释放AVPacket所有资源

avio_closep关闭输出文件IO

avformat_free_context销毁AVFormatContext结构体

 

        


http://www.kler.cn/a/515276.html

相关文章:

  • 解锁C#编程新姿势:Z.ExtensionMethods入门秘籍
  • AIGC视频生成模型:ByteDance的PixelDance模型
  • Java 中的设计模式:经典与现代实践
  • [STM32 HAL库]串口中断编程思路
  • C++ STL(8)map
  • C#深度神经网络(TensorFlow.NET)
  • springboot网上书城
  • android studio本地打包后,无法热更,无法执行换包操作,plus.runtime.install没有弹窗
  • 提升 Go 开发效率的利器:calc_util 工具库
  • 数学规划问题2 .有代码(非线性规划模型,最大最小化模型,多目标规划模型)
  • 项目-03-封装echarts组件并使用component动态加载组件
  • 基于AutoDL云计算平台+LLaMA-Factory训练平台微调本地大模型
  • 网络安全 | 入侵检测系统(IDS)与入侵防御系统(IPS):如何识别并阻止威胁
  • Prolog语言的数据可视化
  • Jpom 安装教程
  • 自动化实现的思路变化
  • 深入解析人工智能中的协同过滤算法及其在推荐系统中的应用与优化
  • [Spring] OpenFeign的使用
  • wx035基于springboot+vue+uniapp的校园二手交易小程序
  • 缓存商品、购物车(day07)
  • JavaScript系列(39)-- Web Workers技术详解
  • 三天急速通关JAVA基础知识:Day3 基础加强
  • Python FastAPI 实战应用指南
  • WordPress Hunk Companion插件节点逻辑缺陷导致Rce漏洞复现(CVE-2024-9707)(附脚本)
  • Nginx:通过upstream进行代理转发
  • vue request 发送formdata