备案号查询网站网址,wordpress.exe,网站建设 百度云,职业技术培训机构本节介绍本章节主要讲解的是push_server_thread线程的具体处理流程#xff0c; push_server_thread这个线程的主要功能是通过时间戳比较#xff0c;来处理音频、视频的数据并最终推流到SRT、RTMP、UDP、RTSP服务器
push_server_thread#xff1a;流程如下 上图#xff0c;… 本节介绍本章节主要讲解的是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…}。
这里要注意的是 在这个推流项目中视频帧率和时间基固定成video_time_base {1,25}video_frame_rate {1,25}。因为底层驱缘故易百纳的摄像头帧率可能只支持25帧所以编码帧率和时间基只能设置{1,25}否则就会导致音视频不同步。 视频VIDEO_PTS和音频AUDIO_PTS需要按照一定的数值规律进行累加。中间不能出现任何的丢失和错误否则就会出现各种问题如花屏、卡顿、音视频不同步等问题。 比方说video_pts {0,3600,7200,14400}这种属于PTS出现丢失 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_a和tb_a作为视频的时间戳和时间基、ts_b和tb_b作为音频的时间戳和时间基。若ret(返回值)0则说明此时要处理视频编码数据就调用deal_video_avpacket函数进行视频编码数据的写入否则就调用deal_audio_avpacket进行音频编码数据的写入当这个线程退出后, 先av_write_trailer结束写入文件结束符并释放所有的资源数据(free_stream、avio_closp、avforamt_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 相等直接返回 03. 如果对应的 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 Sizememcpy(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 Sizememcpy(pkt-buf-data, audio_data_packet-buffer, audio_data_packet-audio_frame_size); //rv1126的音频数据赋值到AVPacket datapkt-data pkt-buf-data; // 把pkt-buf-data赋值到pkt-dataif (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结构体