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

相关文章

MySQL 内存使用率常用分析语句

《MySQL内存使用率常用分析语句》用户整理了MySQL内存占用过高的分析方法,涵盖操作系统层确认及数据库层bufferpool、内存模块差值、线程状态、performance_schema性能数据... 目录一、 OS层二、 DB层1. 全局情况2. 内存占js用详情最近连续遇到mysql内存占用过高导致

Java Stream流之GroupBy的用法及应用场景

《JavaStream流之GroupBy的用法及应用场景》本教程将详细介绍如何在Java中使用Stream流的groupby方法,包括基本用法和一些常见的实际应用场景,感兴趣的朋友一起看看吧... 目录Java Stream流之GroupBy的用法1. 前言2. 基础概念什么是 GroupBy?Stream

深度解析Nginx日志分析与499状态码问题解决

《深度解析Nginx日志分析与499状态码问题解决》在Web服务器运维和性能优化过程中,Nginx日志是排查问题的重要依据,本文将围绕Nginx日志分析、499状态码的成因、排查方法及解决方案展开讨论... 目录前言1. Nginx日志基础1.1 Nginx日志存放位置1.2 Nginx日志格式2. 499

Olingo分析和实践之EDM 辅助序列化器详解(最佳实践)

《Olingo分析和实践之EDM辅助序列化器详解(最佳实践)》EDM辅助序列化器是ApacheOlingoOData框架中无需完整EDM模型的智能序列化工具,通过运行时类型推断实现灵活数据转换,适用... 目录概念与定义什么是 EDM 辅助序列化器?核心概念设计目标核心特点1. EDM 信息可选2. 智能类

Olingo分析和实践之OData框架核心组件初始化(关键步骤)

《Olingo分析和实践之OData框架核心组件初始化(关键步骤)》ODataSpringBootService通过初始化OData实例和服务元数据,构建框架核心能力与数据模型结构,实现序列化、URI... 目录概述第一步:OData实例创建1.1 OData.newInstance() 详细分析1.1.1

Olingo分析和实践之ODataImpl详细分析(重要方法详解)

《Olingo分析和实践之ODataImpl详细分析(重要方法详解)》ODataImpl.java是ApacheOlingoOData框架的核心工厂类,负责创建序列化器、反序列化器和处理器等组件,... 目录概述主要职责类结构与继承关系核心功能分析1. 序列化器管理2. 反序列化器管理3. 处理器管理重要方

SpringBoot中六种批量更新Mysql的方式效率对比分析

《SpringBoot中六种批量更新Mysql的方式效率对比分析》文章比较了MySQL大数据量批量更新的多种方法,指出REPLACEINTO和ONDUPLICATEKEY效率最高但存在数据风险,MyB... 目录效率比较测试结构数据库初始化测试数据批量修改方案第一种 for第二种 case when第三种

解决1093 - You can‘t specify target table报错问题及原因分析

《解决1093-Youcan‘tspecifytargettable报错问题及原因分析》MySQL1093错误因UPDATE/DELETE语句的FROM子句直接引用目标表或嵌套子查询导致,... 目录报js错原因分析具体原因解决办法方法一:使用临时表方法二:使用JOIN方法三:使用EXISTS示例总结报错原

Redis中Stream详解及应用小结

《Redis中Stream详解及应用小结》RedisStreams是Redis5.0引入的新功能,提供了一种类似于传统消息队列的机制,但具有更高的灵活性和可扩展性,本文给大家介绍Redis中Strea... 目录1. Redis Stream 概述2. Redis Stream 的基本操作2.1. XADD

MySQL中的LENGTH()函数用法详解与实例分析

《MySQL中的LENGTH()函数用法详解与实例分析》MySQLLENGTH()函数用于计算字符串的字节长度,区别于CHAR_LENGTH()的字符长度,适用于多字节字符集(如UTF-8)的数据验证... 目录1. LENGTH()函数的基本语法2. LENGTH()函数的返回值2.1 示例1:计算字符串