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

相关文章

Python 中的 with open文件操作的最佳实践

《Python中的withopen文件操作的最佳实践》在Python中,withopen()提供了一个简洁而安全的方式来处理文件操作,它不仅能确保文件在操作完成后自动关闭,还能处理文件操作中的异... 目录什么是 with open()?为什么使用 with open()?使用 with open() 进行

Python 迭代器和生成器概念及场景分析

《Python迭代器和生成器概念及场景分析》yield是Python中实现惰性计算和协程的核心工具,结合send()、throw()、close()等方法,能够构建高效、灵活的数据流和控制流模型,这... 目录迭代器的介绍自定义迭代器省略的迭代器生产器的介绍yield的普通用法yield的高级用法yidle

C++ Sort函数使用场景分析

《C++Sort函数使用场景分析》sort函数是algorithm库下的一个函数,sort函数是不稳定的,即大小相同的元素在排序后相对顺序可能发生改变,如果某些场景需要保持相同元素间的相对顺序,可使... 目录C++ Sort函数详解一、sort函数调用的两种方式二、sort函数使用场景三、sort函数排序

Java调用C++动态库超详细步骤讲解(附源码)

《Java调用C++动态库超详细步骤讲解(附源码)》C语言因其高效和接近硬件的特性,时常会被用在性能要求较高或者需要直接操作硬件的场合,:本文主要介绍Java调用C++动态库的相关资料,文中通过代... 目录一、直接调用C++库第一步:动态库生成(vs2017+qt5.12.10)第二步:Java调用C++

kotlin中const 和val的区别及使用场景分析

《kotlin中const和val的区别及使用场景分析》在Kotlin中,const和val都是用来声明常量的,但它们的使用场景和功能有所不同,下面给大家介绍kotlin中const和val的区别,... 目录kotlin中const 和val的区别1. val:2. const:二 代码示例1 Java

Go标准库常见错误分析和解决办法

《Go标准库常见错误分析和解决办法》Go语言的标准库为开发者提供了丰富且高效的工具,涵盖了从网络编程到文件操作等各个方面,然而,标准库虽好,使用不当却可能适得其反,正所谓工欲善其事,必先利其器,本文将... 目录1. 使用了错误的time.Duration2. time.After导致的内存泄漏3. jsO

SpringCloud动态配置注解@RefreshScope与@Component的深度解析

《SpringCloud动态配置注解@RefreshScope与@Component的深度解析》在现代微服务架构中,动态配置管理是一个关键需求,本文将为大家介绍SpringCloud中相关的注解@Re... 目录引言1. @RefreshScope 的作用与原理1.1 什么是 @RefreshScope1.

Python实现无痛修改第三方库源码的方法详解

《Python实现无痛修改第三方库源码的方法详解》很多时候,我们下载的第三方库是不会有需求不满足的情况,但也有极少的情况,第三方库没有兼顾到需求,本文将介绍几个修改源码的操作,大家可以根据需求进行选择... 目录需求不符合模拟示例 1. 修改源文件2. 继承修改3. 猴子补丁4. 追踪局部变量需求不符合很

Spring事务中@Transactional注解不生效的原因分析与解决

《Spring事务中@Transactional注解不生效的原因分析与解决》在Spring框架中,@Transactional注解是管理数据库事务的核心方式,本文将深入分析事务自调用的底层原理,解释为... 目录1. 引言2. 事务自调用问题重现2.1 示例代码2.2 问题现象3. 为什么事务自调用会失效3

找不到Anaconda prompt终端的原因分析及解决方案

《找不到Anacondaprompt终端的原因分析及解决方案》因为anaconda还没有初始化,在安装anaconda的过程中,有一行是否要添加anaconda到菜单目录中,由于没有勾选,导致没有菜... 目录问题原因问http://www.chinasem.cn题解决安装了 Anaconda 却找不到 An