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中的Walrus运算符分析示例详解

《Python中的Walrus运算符分析示例详解》Python中的Walrus运算符(:=)是Python3.8引入的一个新特性,允许在表达式中同时赋值和返回值,它的核心作用是减少重复计算,提升代码简... 目录1. 在循环中避免重复计算2. 在条件判断中同时赋值变量3. 在列表推导式或字典推导式中简化逻辑

Java Stream流使用案例深入详解

《JavaStream流使用案例深入详解》:本文主要介绍JavaStream流使用案例详解,本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友参考下吧... 目录前言1. Lambda1.1 语法1.2 没参数只有一条语句或者多条语句1.3 一个参数只有一条语句或者多

Java程序进程起来了但是不打印日志的原因分析

《Java程序进程起来了但是不打印日志的原因分析》:本文主要介绍Java程序进程起来了但是不打印日志的原因分析,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录Java程序进程起来了但是不打印日志的原因1、日志配置问题2、日志文件权限问题3、日志文件路径问题4、程序

Java 正则表达式URL 匹配与源码全解析

《Java正则表达式URL匹配与源码全解析》在Web应用开发中,我们经常需要对URL进行格式验证,今天我们结合Java的Pattern和Matcher类,深入理解正则表达式在实际应用中... 目录1.正则表达式分解:2. 添加域名匹配 (2)3. 添加路径和查询参数匹配 (3) 4. 最终优化版本5.设计思

Java字符串操作技巧之语法、示例与应用场景分析

《Java字符串操作技巧之语法、示例与应用场景分析》在Java算法题和日常开发中,字符串处理是必备的核心技能,本文全面梳理Java中字符串的常用操作语法,结合代码示例、应用场景和避坑指南,可快速掌握字... 目录引言1. 基础操作1.1 创建字符串1.2 获取长度1.3 访问字符2. 字符串处理2.1 子字

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