IJKPLAYER源码分析-主要队列

news2024/12/23 4:46:02

 前言

    对IJKPLAYER播放器内核几个关键的队列的理解,将有助于掌控全局。因此,这里简要介绍所涉及到的几个关键队列实现:

  • PacketQueue:压缩数据队列,是一个带有首尾指针和回收单链表头指针的单链表结构,用来实现了队列,包括video、audio和subtitle均用此队列结构存储;
  • FrameQueue:解压数据队列,是一个由数组表达的环形队列,同样包括video、audio和subtitle均用此队列存储;
  • MessageQueue:其结构如同PacketQueue,不再赘述;

PacketQueue

    队列结构

    PacketQueue结构代码定义: 

typedef struct MyAVPacketList {
    AVPacket pkt;
    struct MyAVPacketList *next;
    int serial;
} MyAVPacketList;

typedef struct PacketQueue {
    MyAVPacketList *first_pkt, *last_pkt;
    int nb_packets;
    int size;
    int64_t duration;
    int abort_request;
    int serial;
    SDL_mutex *mutex;
    SDL_cond *cond;
} PacketQueue;
  • first_pkt:队列头指针,也即单链表的头指针;
  • last_pkt:队列尾指针,也即单链表的尾指针;

  • nb_packets:队列元素个数(AVPacket个数),即first_pkt所指向的单链表元素个数;

  • size:AVPacket *pkt;size += pkt1->pkt.size + sizeof(*pkt1),即该队列所有元素的总字节数,包括AVPacket管理所占空间;

  • duration:#define MIN_PKT_DURATION 15;q->duration += FFMAX(pkt1->pkt.duration, MIN_PKT_DURATION);

  • abort_request:关闭播放器请求,层层传递到队列;

  • serial:主要是针对于点播而言,代表1次不同的seek请求,以此区分seek前后的操作及数据;

队列大小 

    相关PacketQueue队列大小的外围限制源码:

#define MAX_QUEUE_SIZE (15 * 1024 * 1024)

{ "max-buffer-size",                    "max buffer size should be pre-read",
        OPTION_OFFSET(dcc.max_buffer_size), OPTION_INT(MAX_QUEUE_SIZE, 0, MAX_QUEUE_SIZE) },
{ "min-frames",                         "minimal frames to stop pre-reading",
        OPTION_OFFSET(dcc.min_frames),      OPTION_INT(DEFAULT_MIN_FRAMES, MIN_MIN_FRAMES, MAX_MIN_FRAMES) },

inline static void ffp_reset_demux_cache_control(FFDemuxCacheControl *dcc)
{
    dcc->min_frames                = DEFAULT_MIN_FRAMES;
    dcc->max_buffer_size           = MAX_QUEUE_SIZE;
}

    缺省情况下:

  • audio + video + subtitle队列size大小之和,<= max_buffer_size=15M;
  • audio / video / subtitle各自队列大小都 <= MIN_FRAMES=50000个AVPacket,对于video而言,即50000个帧压缩数据;

    以上2种情形,满足一个条件,队列即满。 

        /* if the queue are full, no need to read more */
        if (ffp->infinite_buffer<1 && !is->seek_req &&
#ifdef FFP_MERGE
              (is->audioq.size + is->videoq.size + is->subtitleq.size > MAX_QUEUE_SIZE
#else
              (is->audioq.size + is->videoq.size + is->subtitleq.size > ffp->dcc.max_buffer_size
#endif
            || (   stream_has_enough_packets(is->audio_st, is->audio_stream, &is->audioq, MIN_FRAMES)
                && stream_has_enough_packets(is->video_st, is->video_stream, &is->videoq, MIN_FRAMES)
                && stream_has_enough_packets(is->subtitle_st, is->subtitle_stream, &is->subtitleq, MIN_FRAMES)))) {
            if (!is->eof) {
                ffp_toggle_buffering(ffp, 0);
            }
            /* wait 10 ms */
            SDL_LockMutex(wait_mutex);
            SDL_CondWaitTimeout(is->continue_read_thread, wait_mutex, 10);
            SDL_UnlockMutex(wait_mutex);
            continue;
        }

何时继续读

     由上面分析可知,队列满后,会暂停调用av_read_frame读取AVPacket包,而是条件等待10ms,若is->continue_read_thread信号就绪,会返回以继续调用av_read_frame读AVPacket包;或者,10ms超时该信号仍未就绪,也会立刻返回,下一次loop判断队列是否满,若满则继续10ms等待上述逻辑,不然便会调用av_read_frame方法读取下一个AVPacket包。

    那么,continue_read_thread信号何时就绪呢?我们来继续分析:

static void decoder_init(Decoder *d, AVCodecContext *avctx, PacketQueue *queue, SDL_cond *empty_queue_cond) {
    memset(d, 0, sizeof(Decoder));
    d->avctx = avctx;
    d->queue = queue;
    d->empty_queue_cond = empty_queue_cond;
    d->start_pts = AV_NOPTS_VALUE;

    d->first_frame_decoded_time = SDL_GetTickHR();
    d->first_frame_decoded = 0;

    SDL_ProfilerReset(&d->decode_profiler, -1);
}

decoder_init(&is->auddec, avctx, &is->audioq, is->continue_read_thread);

    在初始化video解码器时,会调用decoder_init方法将 is->continue_read_thread赋值给Decoder的empty_queue_cond信号。Decoder的定义:

typedef struct Decoder {
    AVPacket pkt;
    AVPacket pkt_temp;
    PacketQueue *queue;
    AVCodecContext *avctx;
    int pkt_serial;
    int finished;
    int packet_pending;
    int bfsc_ret;
    uint8_t *bfsc_data;

    SDL_cond *empty_queue_cond;
    int64_t start_pts;
    AVRational start_pts_tb;
    int64_t next_pts;
    AVRational next_pts_tb;
    SDL_Thread *decoder_tid;

    SDL_Profiler decode_profiler;
    Uint64 first_frame_decoded_time;
    int    first_frame_decoded;
} Decoder;

     那么,何时发送is->empty_queue_cond信号呢?其实,是在video的解码线程里,其函数调用栈如下:

ffplay_video_thread => get_video_frame => decoder_decode_frame

    而后,在decoder_decode_frame方法里判断: 

if (d->queue->nb_packets == 0)
    SDL_CondSignal(d->empty_queue_cond);

    即:PacketQueue队列中的压缩数据,如已消费完毕便会发送该信号,继续av_read_frame读取AVPacket包,并同时开始缓冲。

    值得一提的是,由于audio、video和subtitle都会发射continue_read_thread信号,因此,此3者只要1个消费完毕,即会发送此信号,并开始缓冲。

播放缓冲

    PacketQueue的多少还会涉及到播放状态,若没有数据了,即开始加载,若队列满,则缓冲完毕,开始render播放。

    播放所涉及到的2个缓冲状态:

#define FFP_MSG_BUFFERING_START             500
#define FFP_MSG_BUFFERING_END               501
  • FFP_MSG_BUFFERING_START:无数据了,开始缓冲;
  • FFP_MSG_BUFFERING_END:队列满,缓冲完毕; 

    那么,在哪里通知开始缓冲的呢?有几种情况:

  • 解码时队列消费完毕:
  • seek请求:avformat_seek_file调用前后,开始seek了;

  • 重播:ijkmp_start_l => ffp_start_from_l;

    第1种情况调用栈:

ffplay_video_thread > get_video_frame => decoder_decode_frame => packet_queue_get_or_buffering => ffp_toggle_buffering(ffp, 1);

    第2种情况调用栈:

read_thread => if (is->seek_req) => ffp_toggle_buffering(ffp, 1) =>  avformat_seek_file => ffp_toggle_buffering(ffp, 1)

    第3种情况调用栈:

int ffp_start_from_l(FFPlayer *ffp, long msec)
{
    assert(ffp);
    VideoState *is = ffp->is;
    if (!is)
        return EIJK_NULL_IS_PTR;

    ffp->auto_resume = 1;
    ffp_toggle_buffering(ffp, 1);
    ffp_seek_to_l(ffp, msec);
    return 0;
}

     那么,何时通知缓冲完毕呢?有以下情况:

  • 缓冲队列满
  • 播放完毕
  • av_read_frame返回eof,即读取结束
  • 检查buffer状态发现队列满

FrameQueue

主结构

     FrameQueue结构代码定义: 

#define FRAME_QUEUE_SIZE 16
/* Common struct for handling all types of decoded data and allocated render buffers. */
typedef struct Frame {
    AVFrame *frame;
    AVSubtitle sub;
    int serial;
    double pts;           /* presentation timestamp for the frame */
    double duration;      /* estimated duration of the frame */
    int64_t pos;          /* byte position of the frame in the input file */
    int width;
    int height;
    int format;
    AVRational sar;
    int uploaded;
    int flip_v;
} Frame;

typedef struct FrameQueue {
    Frame queue[FRAME_QUEUE_SIZE];
    int rindex;
    int windex;
    int size;
    int max_size;
    int keep_last;
    int rindex_shown;
    SDL_mutex *mutex;
    SDL_cond *cond;
    PacketQueue *pktq;
} FrameQueue;
  •  queue[FRAME_QUEUE_SIZE]:是一个有FRAME_QUEUE_SIZE个Frame的数组,实际是环形队列,用以存储AVFrame,即解码后的video、audio和subtitle数据存在此处;
  • rindex:指向最近一个可读的Frame;
  • windex:指向下一个可写的Frame;
  • size:可display的帧个数;
  • max_size:queue环形队列最多存储FRAME_QUEUE_SIZE个Frame,但实际用多少个Frame的空间,是可以配置的,video缺省是3个Frame;
  • keep_last:此flag仅用于video,因为video显示时需缓存上次已显示的AVFrame,以计算duration,在队列初始化时设为1;audio和subtitle不需关注,缺省是0;
  • rindex_shown:与keep_last配合使用,仅用于video和audio,rindex + rindex_shown指向即将播放的帧,初始值为0,待显示1帧后,此值更新为1,后续不再变化;

  • pkt:FrameQueue所关联的PacketQueue;

keep_last & rindex_shown

    此2值仅用于video和audio,subtitle不用关注。

    keep_last在此处初始化:

static int frame_queue_init(FrameQueue *f, PacketQueue *pktq, int max_size, int keep_last)
{
    int i;
    memset(f, 0, sizeof(FrameQueue));
    if (!(f->mutex = SDL_CreateMutex())) {
        av_log(NULL, AV_LOG_FATAL, "SDL_CreateMutex(): %s\n", SDL_GetError());
        return AVERROR(ENOMEM);
    }
    if (!(f->cond = SDL_CreateCond())) {
        av_log(NULL, AV_LOG_FATAL, "SDL_CreateCond(): %s\n", SDL_GetError());
        return AVERROR(ENOMEM);
    }
    f->pktq = pktq;
    f->max_size = FFMIN(max_size, FRAME_QUEUE_SIZE);
    f->keep_last = !!keep_last;
    for (i = 0; i < f->max_size; i++)
        if (!(f->queue[i].frame = av_frame_alloc()))
            return AVERROR(ENOMEM);
    return 0;
}

    在FrameQueue队列初始化,最后一个参数keep_last,video和audio传入1,而subtitle传入0:

    /* start video display */
    if (frame_queue_init(&is->pictq, &is->videoq, ffp->pictq_size, 1) < 0)
        goto fail;
    if (frame_queue_init(&is->subpq, &is->subtitleq, SUBPICTURE_QUEUE_SIZE, 0) < 0)
        goto fail;
    if (frame_queue_init(&is->sampq, &is->audioq, SAMPLE_QUEUE_SIZE, 1) < 0)
        goto fail;

    而后,仅在此方法里使用,其中rindex_shown在frame_queue_init初始化时设置为0:

static void frame_queue_next(FrameQueue *f)
{
    if (f->keep_last && !f->rindex_shown) {
        f->rindex_shown = 1;
        return;
    }
    frame_queue_unref_item(&f->queue[f->rindex]);
    if (++f->rindex == f->max_size)
        f->rindex = 0;
    SDL_LockMutex(f->mutex);
    f->size--;
    SDL_CondSignal(f->cond);
    SDL_UnlockMutex(f->mutex);
}

    而frame_queue_next方法在video或audio播放1帧之后调用,指向FrameQueue的下1个待播放的帧。 

    所以,frame_queue_peek方法实际返回的是下一个待播放的video或audio帧:

static Frame *frame_queue_peek(FrameQueue *f)
{
    return &f->queue[(f->rindex + f->rindex_shown) % f->max_size];
}

    而frame_queue_peek_next方法返回的是下下一个待播放的video或audio帧:    

static Frame *frame_queue_peek_next(FrameQueue *f)
{
    return &f->queue[(f->rindex + f->rindex_shown + 1) % f->max_size];
}

    而frame_queue_peek_last方法返回的是上一个已播放的video或audio帧:

static Frame *frame_queue_peek_last(FrameQueue *f)
{
    return &f->queue[f->rindex];
}

    那么,加入FrameQueue队列写满了,会怎么样?

static Frame *frame_queue_peek_writable(FrameQueue *f)
{
    /* wait until we have space to put a new frame */
    SDL_LockMutex(f->mutex);
    while (f->size >= f->max_size &&
           !f->pktq->abort_request) {
        SDL_CondWait(f->cond, f->mutex);
    }
    SDL_UnlockMutex(f->mutex);

    if (f->pktq->abort_request)
        return NULL;

    return &f->queue[f->windex];
}

    可以看到,如FrameQueue队列写满了,将条件等待该队列非满,方能写入1个帧。

    假如FrameQueue没有可读Frame,是空的,会怎么样呢?

static Frame *frame_queue_peek_readable(FrameQueue *f)
{
    /* wait until we have a readable a new frame */
    SDL_LockMutex(f->mutex);
    while (f->size - f->rindex_shown <= 0 &&
           !f->pktq->abort_request) {
        SDL_CondWait(f->cond, f->mutex);
    }
    SDL_UnlockMutex(f->mutex);

    if (f->pktq->abort_request)
        return NULL;

    return &f->queue[(f->rindex + f->rindex_shown) % f->max_size];
}

     可以看到,若FrameQueue队列空,则条件等待非空,才能读取数据。

MessageQueue

    其源码结构定义:

// based on PacketQueue in ffplay.c

typedef struct AVMessage {
    int what;
    int arg1;
    int arg2;
    void *obj;
    size_t len;
    void (*free_l)(void *obj);
    struct AVMessage *next;
} AVMessage;

typedef struct MessageQueue {
    AVMessage *first_msg, *last_msg;
    int nb_messages;
    int abort_request;
    SDL_mutex *mutex;
    SDL_cond *cond;

    AVMessage *recycle_msg;
    int recycle_count;
    int alloc_count;
} MessageQueue;

    可以看到,此队列是IJKPLAYER参照FFPLAY的PacketQueue而来,因此,不再赘述。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/454696.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

iptables和firewalld防火墙

安全技术和防火墙概述 安全技术 入侵检测系统&#xff08;Intrusion Detection Systems&#xff09;&#xff1a;特点是不阻断任何网络访问&#xff0c;量化、定位来自内外网络的威胁情况&#xff0c;主要以提供报警和事后监督为主&#xff0c;提供有针对性的指导措施和安全决…

SLAM论文速递【SLAM—— 基于平面特征的动态SLAM—4.24(2)

论文信息 题目&#xff1a; Dynamic SLAM Based on Planar Features 基于平面特征的动态SLAM论文地址&#xff1a; https://ieeexplore.ieee.org/abstract/document/9834113发表期刊&#xff1a; 2022 7th International Conference on Automation, Control and Robotics En…

从源码全面解析 ArrayBlockingQueue 的来龙去脉

&#x1f44f;作者简介&#xff1a;大家好&#xff0c;我是爱敲代码的小黄&#xff0c;独角兽企业的Java开发工程师&#xff0c;CSDN博客专家&#xff0c;阿里云专家博主&#x1f4d5;系列专栏&#xff1a;Java设计模式、数据结构和算法、Kafka从入门到成神、Kafka从成神到升仙…

前缀索引,在性能和空间中寻找平衡

文章目录 1.什么是前缀索引2.什么是索引选择性3.创建前缀索引3.1 一个小案例3.2 前缀索引3.3 一个问题 4. 回到开始的问题5.小结 我们在项目的具体实践中&#xff0c;有时候会遇到一些比较特殊的字段&#xff0c;例如身份证号码。 松哥之前有一个小伙伴做黑龙江省的政务服务网&…

手持式激光焊接机多少钱一台

目前很火的一台机器&#xff0c;相比于传统的焊接&#xff0c;手持激光焊接机最大的亮点在于&#xff1a; 1、对于操作工人没有要求&#xff1a;不需要焊工证、男女老少均可 这样可以大大节省人工成本 2、焊接质量 由于他的焊接效率、操作性能比较突出&#xff0c;即便是第一…

C++面试指南——类常用知识点概念总结(附C++进阶视频教程)

构造函数 构造函数可以抛出异常&#xff0c;可以重载&#xff0c;如果在实例化时在类名后面加个括号&#xff0c;只是创建了一个匿名的对象。构造不能是虚函数&#xff0c;因为此时虚函数表还没有初始化。new对象会调解构造函数。没有定义拷贝构造时&#xff0c;IDE会自动生成…

宝塔面板安装配置MySQL,轻松管理数据库【公网远程访问】

文章目录 前言1.Mysql服务安装2.创建数据库3.安装cpolar内网穿透4. 创建HTTP隧道映射mysql端口5.远程连接6.固定TCP地址6.1 保留一个固定的公网TCP端口地址6.2 配置固定公网TCP端口地址 前言 宝塔面板的简易操作性,使得运维难度降低,简化了Linux命令行进行繁琐的配置,下面简单…

软件测试常规测试过程模型——V模型与X模型

一、V模型简单介绍及讲解 V模型是软件测试过程模型中最广为人知的模型&#xff0c;尽管很多富有实际经验的测试人员还是不太熟悉V模型&#xff0c;或者其它的模型。V模型中的过程从左到右&#xff0c;描述了基本的开发过程和测试行为。V模型的价值在于它非常明确地标明了测试过…

SpringBoot整合Minio,一篇带你入门使用Minio

本文介绍SpringBoot如何整合Minio&#xff0c;解决文件存储问题 文章目录 前言环境搭建项目环境搭建添加依赖库yml配置 Docker安装minio 代码实现MiniConfigservicecontroller 测试 前言 参考链接&#xff1a; 官网 环境搭建 项目环境搭建 将minio单独封装成一个module&am…

安全代码审计实施标准学习笔记

声明 本文是学习GB-T 39412-2020 信息安全技术 代码安全审计规范. 而整理的学习笔记,分享出来希望更多人受益,如果存在侵权请及时联系我们 资源使用安全缺陷审计列表 资源管理 审计指标&#xff1a;应避免重复释放资源。 审计人员应检查代码是否存在重复释放资源的情况。重…

Opencv+Python笔记(九)模板匹配

模板匹配 模板匹配常用于对象检测&#xff0c;且实现简单计算效率高。但如果输入图像中存在变化因素如旋转、缩放、视角变化等&#xff0c;模板匹配很容易失效 模板匹配原理&#xff1a; 1.匹配方式为模板 (a * b) 在原图像 (m * n) 上滑动 使用参数method中指定的方法&#…

云原生(docker+k8s+阿里云)

Gitee-Kubernetes学习 kubectl备忘清单 k8s官方文档-task [云原生-kubectl命令详解] ingress详解 ingress官方文档 云原生-语雀-架构师第一课 如上图&#xff0c;服务器有公网ip和私网ip&#xff0c;公网ip是外部访问服务器用的&#xff0c;重启一次实例就变化了&#xff0c;如…

常用数据结构与颜色空间

常用数据结构与颜色空间 矩阵和图像类型 图像可能是灰度&#xff0c;彩色&#xff0c;4 通道的(RGBalpha)&#xff0c;其中每个通道可以包含任意的整数或浮点数。因此&#xff0c;该类型比常见的、易于理解的3通道 8位 RGB 图像更通用。 RBG颜色空间、 HSV/HLS颜色空间、 Lab…

Blender 3.5 面的操作(二)

目录 1. 面操作1.1 面的切割1.2 整体切分1.3 面的法向1.4 正面、背面1.5 翻转法向1.6 填充面1.7 X-Ray 透视模式 1. 面操作 1.1 面的切割 切割工具 Knife&#xff0c;快捷键 k 选中一个面 按k键&#xff0c;进入切割工具&#xff08;建议使用快捷键切割&#xff09;&#xff…

crossover可以安装什么软件?支持的软件列表

CrossOver是一款可以在Mac和Linux等操作系统上运行Windows软件&#xff0c;而无需在计算机上安装Windows操作系统。这款软件的核心技术是Wine&#xff0c;它是一种在Linux和macOS等操作系统上运行Windows应用程序的开源软件。本文将会对CrossOver进行详细介绍&#xff0c;并回答…

“SCSA-T学习导图+”系列:IPSec VPN原理与应用

本期引言&#xff1a; 本章主要讲解IPSec VPN相关理论概念&#xff0c;工作原理。从安全和加密原理入手&#xff0c;讲解了IPSec 在VPN对等体设备实现的安全特性&#xff0c;如数据的机密性、数据的完整性&#xff0c;数据验证等。重点分析IPSec封装模式&#xff0c;IPSec安全…

技术解读丨多模数据湖:助力AI技术,推动内容管理平台智能化升级

随着数字化时代的到来&#xff0c;数据已经成为企业的重要资产之一。因此&#xff0c;构建高效的内容管理平台变得至关重要。本文重点介绍SequoiaDB多模数据湖技术在内容管理平台中的应用和成效&#xff0c;以及其对企业非结构化数据管理和AI的推动作用。 随着数字化时代的到来…

Vue3技术6之toRef和toRefs、shallowReactive与shallowRef、readonly与shallowReadonly

Vue3技术6 toRef和toRefstoRefApp.vueDemo.vue toRefsApp.vueDemoTwo.vue 总结 shallowReactive与shallowRefshallowReactiveApp.vueDemo.vue shallowRefDemo.vue 总结 readonly与shallowReadonlyApp.vueDemo.vueDemoTwo.vue总结 toRef和toRefs toRef App.vue <template&…

SpringCloud入门实战(七)-Hystrix服务限流

&#x1f4dd; 学技术、更要掌握学习的方法&#xff0c;一起学习&#xff0c;让进步发生 &#x1f469;&#x1f3fb; 作者&#xff1a;一只IT攻城狮 。 &#x1f490;学习建议&#xff1a;1、养成习惯&#xff0c;学习java的任何一个技术&#xff0c;都可以先去官网先看看&…

电子行业数字工厂管理系统的生产管理模式是什么

随着电子行业的不断发展&#xff0c;数字工厂管理系统在生产管理中的应用越来越广泛。数字工厂系统是一种综合管理系统&#xff0c;它将企业的采购、生产、销售、财务、人力资源等多个方面进行整合&#xff0c;实现了企业资源的有效整合和管理效率的提升。电子行业数字工厂系统…