FFplay源码分析-stream_component_open

2024-06-24 01:58

本文主要是介绍FFplay源码分析-stream_component_open,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

《FFmpeg原理》的社群来了,想加入社群的朋友请购买 VIP 版,VIP 版有更高级的内容与答疑服务。


本系列 以 ffmpeg4.2 源码为准,下载地址:链接:百度网盘 提取码:g3k8

FFplay 源码分析系列以一条简单的命令开始,ffplay -i a.mp4。a.mp4下载链接:百度网盘,提取码:nl0s 。


上一篇文章已经讲解完了 stream_component_open() 的逻辑,这篇文章主要讲解 audio_thread(),音频解码线程的内部逻辑。

在这里插入图片描述

因为解码线程里面涉及到了 struct Decoder,PacketQueue,FrameQueue的操作,所以必须先简单介绍一下这些数据结构的关系。

在这里插入图片描述

struct Decoder

  • AVPacket pkt; 缓存包,在 avcodec_send_packet() 返回 AVERROR(EAGAIN) 的时候用的,因为AVPacket已经从队列里面拿出来了,如果send给解码器的时候失败了,就需要把拿出来的AVPacket放到 Decoder::pkt 里面,要不AVPacket就会丢失,同时 packet_pending 置为 1,下次优先取Decoder::pkt 来 send给解码器。请看代码。

    ffplay.c 668 ~ 672行
    if (avcodec_send_packet(d->avctx, &pkt) == AVERROR(EAGAIN)) {av_log(d->avctx, AV_LOG_ERROR, "Receive_frame and send_packet both returned EAGAIN, which is an API violation.\n");d->packet_pending = 1; //注意这行代码。av_packet_move_ref(&d->pkt, &pkt); //注意这行代码。
    }
    ffplay.c 637 ~ 647行
    do {if (d->queue->nb_packets == 0)SDL_CondSignal(d->empty_queue_cond);if (d->packet_pending) { //注意这行代码。av_packet_move_ref(&pkt, &d->pkt);d->packet_pending = 0;} else {if (packet_queue_get(d->queue, &pkt, 1, &d->pkt_serial) < 0)return -1;}
    } while (d->queue->serial != d->pkt_serial);
    
  • PacketQueue *queue; 队列。

  • AVCodecContext *avctx; 解码器上下文

  • int pkt_serial; 解码器包序列,这个变量是每次从队列里面取出一个 MyAVPacketList,都会把 pkt_serial 设置为 MyAVPacketList::serial,可以理解为,这个变量是最后一个发送给解码器的 packet 的序列号。

  • int finished; 解码器是否已经没有 AVFrame可以输出。在 avcodec_receive_frame() 返回 AVERROR_EOF 的时候,finished 会设置为 pkt_serial,就是设置为最后一个packet的序列号。

  • int packet_pending; 在 avcodec_send_packet() 返回 AVERROR(EAGAIN) 的时候用的

  • SDL_cond *empty_queue_cond; empty_queue_cond 条件变量其实等于 continue_read_thread,请看下面代码。上一篇文章分析 read_thread() 的时候,read_thread() 在某些情况下会休眠10ms,例如队列满了或超过最小缓存size,会休眠10ms,如果在5ms的时候,队列已经被消耗完了,没有frame可以播放了,那就需要尽快唤醒read_thread() 线程,这个 empty_queue_cond 条件变量就是用来唤醒 read_thread 线程的。

    //read_thread 休眠 10ms
    SDL_CondWaitTimeout(is->continue_read_thread, wait_mutex, 10);
    //注意最后一个参数,continue_read_thread 赋值给 empty_queue_cond 
    decoder_init(&is->auddec, avctx, &is->audioq, is->continue_read_thread);
    decoder_init(&is->viddec, avctx, &is->videoq, is->continue_read_thread);
    
  • int64_t start_pts; 这个字段只用于音频流,在本文命令里面,这个字段的值一直是 AV_NOPTS_VALUE,不清楚在音频流什么场景下使用。

  • AVRational start_pts_tb; 时间基。

  • int64_t next_pts; 这个字段也是只用于音频流了。从 avcodec_receive_frame() 解码器取到的AVFrame 的pts如果有问题,就会用next_pts代替。next_pts的计算方式是上一帧的pts + 上一帧的样本数。如下代码所示:

    ret = avcodec_receive_frame(d->avctx, frame);
    if (ret >= 0) {AVRational tb = (AVRational){1, frame->sample_rate};if (frame->pts != AV_NOPTS_VALUE)frame->pts = av_rescale_q(frame->pts, d->avctx->pkt_timebase, tb);else if (d->next_pts != AV_NOPTS_VALUE) //注意这行代码frame->pts = av_rescale_q(d->next_pts, d->next_pts_tb, tb);if (frame->pts != AV_NOPTS_VALUE) {d->next_pts = frame->pts + frame->nb_samples;d->next_pts_tb = tb;}
    }
    
  • AVRational next_pts_tb; 时间基

  • SDL_Thread *decoder_tid; 解码线程 id。

struct PacketQueue

  • MyAVPacketList *first_pkt, *last_pkt; 队列的头尾
  • int nb_packets; 队列的包数量
  • int size; 队列缓存的数据size
  • int64_t duration; 队列缓存的duration,通过AVPacket->duration 累加得到。
  • int abort_request; 停止解码线程,解码线程代码有好几个地方判断 abort_request 是否为1, stream_component_close() 里面会把这个字段置为1。
  • int serial; 队列的序列号,队列的serial跟 MyAVPacketList 的serial 可以不一样的。跟队列serial不一样的 MyAVPacketList 就是旧的,解码的时候会丢弃旧的MyAVPacketList。
  • SDL_mutex *mutex; 互斥锁,主要用于修改队列的时候加锁。
  • SDL_cond *cond; 条件变量,用于解码线程跟 read_thread 线程通信。当解码线程没有packet可以读的时候,就会 wait cond 阻塞,等待,然后 read_thread 读取到packet丢进去队列之后,就会signal cond 唤醒解码线程继续解码。

预备工作,数据结构已经讲完了,下面正式开始讲解 audio_thread() 解码线程里面的逻辑。

static int audio_thread(void *arg)
{VideoState *is = arg;AVFrame *frame = av_frame_alloc();...省略代码..do {if ((got_frame = decoder_decode_frame(&is->auddec, frame, NULL)) < 0)goto the_end;...省略代码...} while (ret >= 0 || ret == AVERROR(EAGAIN) || ret == AVERROR_EOF);
}

从上面的代码可以看到 audio_thread() 一开始就进入一个 do() while{} 的循环。在这个循环里面会不断的拿 PacketQueue 的数据,传给解码器,解出 AVFrame,然后把 AVFrame 过一遍filter,本文命令filter是空,然后再把 frame 插入 FrameQueue 队列,让播放线程取拿frame播放。

里面比较重要的一个函数是 decoder_decode_frame(),这是一个阻塞函数,它内部会不断循环等待,直到读出一个AVFrame。

重点知识:

decoder_decode_frame()返回的 got_frame 的有几个值?。

  • 返回 1,获取到 AVFrame了。
  • 返回 0 ,文件已经读取完毕,并且也解码完毕,没有AVFrame返回。
  • 返回 -1,流关闭了,abort_request 变成了 1了,got_frame 就会是 -1。

下面分析获取到 AVFrame 之后的逻辑。

获取到 AVFrame 之后,ffplay 会对 AVFrame的采样率,格式做一次校验,这里又有一次校验,ffplay真是严谨。主要校验什么呢?校验stream容器层的采样率,格式,是不是跟 AVFrame 的数据是一致的?应该是ffpaly担心,有些音频流,虽然容器层的采样率字段是48000,但实际解码出来的AVFrame 却是 44100。校验如果不一致,就会 调 configure_audio_filters() 重新初始filter。

校验代码如下:

reconfigure =cmp_audio_fmts(is->audio_filter_src.fmt, is->audio_filter_src.channels,frame->format, frame->channels) ||is->audio_filter_src.channel_layout != dec_channel_layout ||is->audio_filter_src.freq           != frame->sample_rate ||is->auddec.pkt_serial               != last_serial;

is->audio_filter_src 的值是从解码器 avctx 里面来的,请看下图。

ffpaly.c 2639 行
is->audio_filter_src.freq           = avctx->sample_rate;
is->audio_filter_src.channels       = avctx->channels;
is->audio_filter_src.channel_layout = get_valid_channel_layout(avctx->channel_layout, avctx->channels);
is->audio_filter_src.fmt            = avctx->sample_fmt;

解码器 avctx 的采样率又是从容器层来的,请看以下代码:

ffpaly.c 2581 行
ret = avcodec_parameters_to_context(avctx, ic->streams[stream_index]->codecpar);

校验完采样率之后,就是调 av_buffersrc_add_frame() 把 AVFrame 往 filter 里面送了,然后 循环 调 av_buffersink_get_frame_flags(),不断收割经过filter的AVFrame。然后调 frame_queue_push() 插入 FrameQueue 队列,让播放线程取。


ffplay 源码分析,audio_thread() 解码线程分析完毕。

©版权所属:弦外之音。

由于笔者的水平有限, 加之编写的同时还要参与开发工作,文中难免会出现一些错误或者不准确的地方,恳请读者批评指正。

这篇关于FFplay源码分析-stream_component_open的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1088871

相关文章

Nginx分布式部署流程分析

《Nginx分布式部署流程分析》文章介绍Nginx在分布式部署中的反向代理和负载均衡作用,用于分发请求、减轻服务器压力及解决session共享问题,涵盖配置方法、策略及Java项目应用,并提及分布式事... 目录分布式部署NginxJava中的代理代理分为正向代理和反向代理正向代理反向代理Nginx应用场景

Redis中的有序集合zset从使用到原理分析

《Redis中的有序集合zset从使用到原理分析》Redis有序集合(zset)是字符串与分值的有序映射,通过跳跃表和哈希表结合实现高效有序性管理,适用于排行榜、延迟队列等场景,其时间复杂度低,内存占... 目录开篇:排行榜背后的秘密一、zset的基本使用1.1 常用命令1.2 Java客户端示例二、zse

Redis中的AOF原理及分析

《Redis中的AOF原理及分析》Redis的AOF通过记录所有写操作命令实现持久化,支持always/everysec/no三种同步策略,重写机制优化文件体积,与RDB结合可平衡数据安全与恢复效率... 目录开篇:从日记本到AOF一、AOF的基本执行流程1. 命令执行与记录2. AOF重写机制二、AOF的

MyBatis Plus大数据量查询慢原因分析及解决

《MyBatisPlus大数据量查询慢原因分析及解决》大数据量查询慢常因全表扫描、分页不当、索引缺失、内存占用高及ORM开销,优化措施包括分页查询、流式读取、SQL优化、批处理、多数据源、结果集二次... 目录大数据量查询慢的常见原因优化方案高级方案配置调优监控与诊断总结大数据量查询慢的常见原因MyBAT

分析 Java Stream 的 peek使用实践与副作用处理方案

《分析JavaStream的peek使用实践与副作用处理方案》StreamAPI的peek操作是中间操作,用于观察元素但不终止流,其副作用风险包括线程安全、顺序混乱及性能问题,合理使用场景有限... 目录一、peek 操作的本质:有状态的中间操作二、副作用的定义与风险场景1. 并行流下的线程安全问题2. 顺

MyBatis/MyBatis-Plus同事务循环调用存储过程获取主键重复问题分析及解决

《MyBatis/MyBatis-Plus同事务循环调用存储过程获取主键重复问题分析及解决》MyBatis默认开启一级缓存,同一事务中循环调用查询方法时会重复使用缓存数据,导致获取的序列主键值均为1,... 目录问题原因解决办法如果是存储过程总结问题myBATis有如下代码获取序列作为主键IdMappe

Java中最全最基础的IO流概述和简介案例分析

《Java中最全最基础的IO流概述和简介案例分析》JavaIO流用于程序与外部设备的数据交互,分为字节流(InputStream/OutputStream)和字符流(Reader/Writer),处理... 目录IO流简介IO是什么应用场景IO流的分类流的超类类型字节文件流应用简介核心API文件输出流应用文

java 恺撒加密/解密实现原理(附带源码)

《java恺撒加密/解密实现原理(附带源码)》本文介绍Java实现恺撒加密与解密,通过固定位移量对字母进行循环替换,保留大小写及非字母字符,由于其实现简单、易于理解,恺撒加密常被用作学习加密算法的入... 目录Java 恺撒加密/解密实现1. 项目背景与介绍2. 相关知识2.1 恺撒加密算法原理2.2 Ja

Nginx屏蔽服务器名称与版本信息方式(源码级修改)

《Nginx屏蔽服务器名称与版本信息方式(源码级修改)》本文详解如何通过源码修改Nginx1.25.4,移除Server响应头中的服务类型和版本信息,以增强安全性,需重新配置、编译、安装,升级时需重复... 目录一、背景与目的二、适用版本三、操作步骤修改源码文件四、后续操作提示五、注意事项六、总结一、背景与

Android实现图片浏览功能的示例详解(附带源码)

《Android实现图片浏览功能的示例详解(附带源码)》在许多应用中,都需要展示图片并支持用户进行浏览,本文主要为大家介绍了如何通过Android实现图片浏览功能,感兴趣的小伙伴可以跟随小编一起学习一... 目录一、项目背景详细介绍二、项目需求详细介绍三、相关技术详细介绍四、实现思路详细介绍五、完整实现代码