百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 编程字典 > 正文

FFplay超详细数据结构分析(ffplay.c)

toyiye 2024-07-03 02:16 13 浏览 0 评论

阅读本文前,可以看看前面的文章。

FFmpeg的FFplay框架分析




struct VideoState

这个结构相当于一个总的入口,ffplay所有的函数,结构基本在这里都能够找到。相当于是一个manger。

typedef struct VideoState {
  //读线程id
    SDL_Thread *read_tid;
  //一般为空,指向解封装器
    AVInputFormat *iformat;
  //这个值为1,表示请求退出播放,退出播放器一般会把这个值置为1。
    int abort_request;
  //这个值为1,表示请求?即刷新画?
    int force_refresh;
  // =1时暂停,=0时播放
    int paused;
  // 暂存“暂停”/“播放”状态
    int last_paused;
  //主要是作为一个音频或视频的封面的请求标志,比如Mp3专辑的封面
    int queue_attachments_req;
  // 标识?次seek请求,每次seek请求都会标志
    int seek_req;
  //seek标志,是按照字节还是时间,如AVSEEK_FLAG_BYTE等
    int seek_flags;
  // 请求seek的?标位置(当前位置+增量),这个是绝对位置。
    int64_t seek_pos;
  //本次seek的位置增量,这个相当于是相对位置。
    int64_t seek_rel;
  //读取暂停状态
    int read_pause_return;
  //解复用器上下文
    AVFormatContext *ic;
  //为1表示实时流
    int realtime;
//?频时钟
    Clock audclk;
  //视频时钟
    Clock vidclk;
  // 外部时钟
    Clock extclk;
// 视频Frame队列
    FrameQueue pictq;
  // 字幕Frame队列
    FrameQueue subpq;
  // 采样Frame队列
    FrameQueue sampq;
// ?频解码器
    Decoder auddec;
  // 视频解码器
    Decoder viddec;
  // 字幕解码器
    Decoder subdec;
// ?频流索引
    int audio_stream;
// ?视频同步类型, 默认audio master
    int av_sync_type;
// 当前?频帧的PTS+当前帧Du ration
    double audio_clock;
  // 播放序列,seek可改变此值
    int audio_clock_serial;
  //音频平均值差值
    double audio_diff_cum; /* used for AV difference average computation */
    double audio_diff_avg_coef;
  //音频门限值  
  double audio_diff_threshold;
    int audio_diff_avg_count;
  // ?频流
    AVStream *audio_st;
  // ?频packet队列
    PacketQueue audioq;
  // SDL?频缓冲区的??(字节 为单位)
    int audio_hw_buf_size;
  /* 指向待播放的?帧?频数据,指向的数据区将被拷?SDL?频缓冲区。
  若经过重采样 则指向audio_buf1,否则指向frame中的?频*/
    uint8_t *audio_buf;
  // 指向重采样后的数据
    uint8_t *audio_buf1;
  // 待播放的?帧?频数据(aud io_buf指向)的??
    unsigned int audio_buf_size; /* in bytes */
  //申请到的?频缓冲区(重采样区域)audio_ buf1的实际尺?
    unsigned int audio_buf1_size;
  // 更新拷?位置, 当前?频帧中 已拷?SDL?频缓冲区的位置索引(指向第?个待拷 ?字节)
    int audio_buf_index; /* in bytes */
  // 当前?频帧中尚未拷?SDL?频缓冲区的数据量:
  // audio_buf_size = audio_buf_index + audio_write_buf_size。
    int audio_write_buf_size;
  // ?量
    int audio_volume;
  // =1静?,=0则正常
    int muted;
  // ?频frame的参数
    struct AudioParams audio_src;
#if CONFIG_AVFILTER
    struct AudioParams audio_filter_src;
#endif
// SDL?持的?频参数,重采样转 换:audio_src->audio_tgt
    struct AudioParams audio_tgt;
  // ?频重采样上下文
    struct SwrContext *swr_ctx;
  // 丢弃视频packet计数
    int frame_drops_early;
  // 丢弃视频frame计数
    int frame_drops_late;
//这个是显示模式,是都显示,还是只显示音频或视频
    enum ShowMode {
        SHOW_MODE_NONE = -1, SHOW_MODE_VIDEO = 0, SHOW_MODE_WAVES, SHOW_MODE_RDFT, SHOW_MODE_NB
    } show_mode;
  //?频波形显示使?,这里一般是用作频谱图,暂时不作分析。
    int16_t sample_array[SAMPLE_ARRAY_SIZE];
    int sample_array_index;
    int last_i_start;
    RDFTContext *rdft;
    int rdft_bits;
    FFTSample *rdft_data;
    int xpos;
    double last_vis_time;
    SDL_Texture *vis_texture;
  // 字幕显示
    SDL_Texture *sub_texture;
  // 视频显示
    SDL_Texture *vid_texture;
// 字幕流索引
    int subtitle_stream;
  // 字幕流
    AVStream *subtitle_st;
  // 字幕packet队列
    PacketQueue subtitleq;
// 记录最后?帧播放的时刻
    double frame_timer;
  
    double frame_last_returned_time;
    double frame_last_filter_delay;
  // 视频流索引
    int video_stream;
  // 视频流
    AVStream *video_st;
  // 视频队列
    PacketQueue videoq;
  // ?帧最?间隔
    double max_frame_duration;      // maximum duration of a frame - above this, we consider the jump a timestamp discontinuity
  //视频尺?格式变换
  struct SwsContext *img_convert_ctx;
  //字幕尺?格式变换
    struct SwsContext *sub_convert_ctx;
 // 是否读取结束
  int eof;
// ?件名
    char *filename;
   // 宽、?,x起始坐标,y起始坐标
    int width, height, xleft, ytop;
  // =1 步进播放模式, =0 其他模式
    int step;
//一些过滤器的设计,这是一个功能比较齐全的播放器。
#if CONFIG_AVFILTER
    int vfilter_idx;
    AVFilterContext *in_video_filter;   // the first filter in the video chain
    AVFilterContext *out_video_filter;  // the last filter in the video chain
    AVFilterContext *in_audio_filter;   // the first filter in the audio chain
    AVFilterContext *out_audio_filter;  // the last filter in the audio chain
    AVFilterGraph *agraph;              // audio filter graph
#endif
// 保留最近的相应audio、video、subtitle流的steam index
//如果是有多种语言,可能有多个音频流
    int last_video_stream, last_audio_stream, last_subtitle_stream;
// 当读取数据队列满了后进?休眠时,可 以通过该condition唤醒读线程,条件变量
  //seek是在数据读取的线程里做的
    SDL_cond *continue_read_thread;
} VideoState;

时钟讲解

typedef struct Clock {
  // 时钟基础, 当前帧(待播放)显示时间戳,播放后, 当前帧变成上?帧
    double pts;           /* clock base */
  // 当前pts与当前系统时钟的差值, audio、video对于该值是独?
    double pts_drift;     /* clock base minus time at which we updated the clock */
  // 当前时钟(如视频时钟)最后?次更新时间,也可称当前时钟时间
    double last_updated;
  // 时钟速度控制,?于控制播放速度
    double speed;
  //每一帧对应的播放序列
    int serial;           /* clock is based on a packet with this serial */
  // = 1 说明是暂停状态
    int paused;
  //指向packet_serial
    int *queue_serial;    /* pointer to the current packet queue serial, used for obsolete clock detection */
} Clock;


音视频队列

ffplay?PacketQueue保存解封装后的数据,即保存AVPacket。可以理解为是队列的?个节点。可以通过其 next 字段访问下?个节点。serial字段主要?于标记当前节点的播放序列号,ffplay中多处?到serial的概念,主要?来区分是否连续数据每做?次seek,该serial都会做+1的递增,以区分不同的播放序列。serial字段在我们ffplay的分析中应??常?泛,谨记他是?来区分数据否连续先。

typedef struct MyAVPacketList {
  //解封装后的数据
    AVPacket pkt;
  //下?个节点
    struct MyAVPacketList *next;
  //播放序列,可能包含一组packet
    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;

该结构体内定义了“队列”?身的属性。上?的注释对每个字段作了简单的介绍,这?也看到了serial字段,MyAVPacketList的serial字段的赋值来?PacketQueue的serial,每个PacketQueue的serial是独?的。?频、视频、字幕流都有??独?的PacketQueue。

接下来我们也从队列的操作函数具体分析各个字段的含义。

PacketQueue 操作提供以下?法:

packet_queue_init:初始化

packet_queue_destroy:销毁

packet_queue_start:启?

packet_queue_abort:中?

packet_queue_get:获取?个节点

packet_queue_put:存??个节点

packet_queue_put_nullpacket:存??个空节点

packet_queue_flush:清除队列内所有的节点


packet_queue_init()

初始化?于初始各个字段的值,并创建mutex和cond:

static int packet_queue_init(PacketQueue *q)
{
    memset(q, 0, sizeof(PacketQueue));
  //创建互斥量
    q->mutex = SDL_CreateMutex();
    if (!q->mutex) {
        av_log(NULL, AV_LOG_FATAL, "SDL_CreateMutex(): %s\n", SDL_GetError());
        return AVERROR(ENOMEM);
    }
  //创建条件变量
    q->cond = SDL_CreateCond();
    if (!q->cond) {
        av_log(NULL, AV_LOG_FATAL, "SDL_CreateCond(): %s\n", SDL_GetError());
        return AVERROR(ENOMEM);
    }
  // 在packet_queue_start和packet_queue_abort 时修改到该值,要注意观察到该值的变化
    q->abort_request = 1;
    return 0;
}

packet_queue_destroy()

packet_queue_destroy()销毁过程负责清理mutex和cond,清空所有节点。

static void packet_queue_destroy(PacketQueue *q)
{
  //先清除所有的节点
    packet_queue_flush(q);
  //清除锁
    SDL_DestroyMutex(q->mutex);
  //清除条件变量
    SDL_DestroyCond(q->cond);
}

packet_queue_start()

启动队列

static void packet_queue_start(PacketQueue *q)
{
  //为了多线程,需要锁来维护
    SDL_LockMutex(q->mutex);
  //启动时,这个置为0
    q->abort_request = 0;
    packet_queue_put_private(q, &flush_pkt);
    SDL_UnlockMutex(q->mutex);
}

这?放?了?个flush_pkt,问题:?的是什么?

flush_pkt定义是 static AVPacket flush_pkt; ,是?个特殊的packet,主要?来作为?连续的两端数据的“分界”标记。比如有如下2个功能。

(1)插? flush_pkt 触发PacketQueue其对应的serial,加1操作。

(2)特别是在seek的时候,触发解码器清空?身缓存 avcodec_flush_buffers(),以备新序列的数据进?新解码。只要做seek,packet的缓存和frame的缓存,解码器的缓存都是要释放,否则会有马赛克现象。

packet_queue_abort()

中?队列:

static void packet_queue_abort(PacketQueue *q) 
 { 
   SDL_LockMutex(q->mutex); 
     // 请求退出 
   q->abort_request = 1;
   //释放?个条件信号
    SDL_CondSignal(q->cond);  
    SDL_UnlockMutex(q->mutex); 
 }

这?SDL_CondSignal的作?在于确保当前等待该条件的线程能被激活,并继续执?退出流程,并唤醒者会检测abort_request标志确定??的退出流程。


packet_queue_put()

读、写是PacketQueue的主要?法。先看写——往队列中放??个节点:

static int packet_queue_put(PacketQueue *q, AVPacket *pkt)
{
    int ret;

    SDL_LockMutex(q->mutex);
    ret = packet_queue_put_private(q, pkt);
    SDL_UnlockMutex(q->mutex);

    if (pkt != &flush_pkt && ret < 0)
        av_packet_unref(pkt);

    return ret;
}

主要实现在函数 packet_queue_put_private ,这?需要注意的是如果插?失败,则需要释放AVPacket。再分析packet_queue_put_private:

static int packet_queue_put_private(PacketQueue *q, AVPacket *pkt)
{
    MyAVPacketList *pkt1;
//如果已中?,则放?失败
    if (q->abort_request)
       return -1;
//分配节点内存
    pkt1 = av_malloc(sizeof(MyAVPacketList));
  //内存不?,则放?失败
    if (!pkt1)
        return -1;
  /*
  没有做引?计数,那这?也说明av_read_frame内部不会释放替?户释放buffer。
  拷?AVPacket(浅拷?,AVPacket.data等内存并没有拷?)。它是一个直接赋值操作。
  */
    pkt1->pkt = *pkt;
    pkt1->next = NULL;
  //如果放?的是flush_pkt,需要增加队列的播放序列 号,以区分不连续的两段数据
    if (pkt == &flush_pkt)
      {
      	  q->serial++;
      }
      //?队列序列号标记节点序列号
    pkt1->serial = q->serial;

  /*
  队列操作:如果last_pkt为空,说明队列是空的,新增节点为队头。
  否则,队列有数据,则让原队尾的next为新增节点。 最后将队尾指向新增节点。
  */
    if (!q->last_pkt)
        q->first_pkt = pkt1;
    else
        q->last_pkt->next = pkt1;
  //将队尾指向新增节点
    q->last_pkt = pkt1;
  //队列属性操作:增加节点数、 ?来控制队列的??。都要做出相应改变。
    q->nb_packets++;
  //cache??
    q->size += pkt1->pkt.size + sizeof(*pkt1);
  //cache总时?
    q->duration += pkt1->pkt.duration;
    /* XXX: should duplicate packet data in DV case */
    SDL_CondSignal(q->cond);
    return 0;
}

对于packet_queue_put_private主要完成3件事:

(1)计算serial。serial标记了这个节点内的数据是何时的。?般情况下新增节点与上?个节点的serial是?样的,但当队列中加??个flush_pkt后,后续节点的serial会?之前?1,?来区别不同播放序列的packet

(2)节点?队列操作。使用尾插法。

(3)队列属性操作。更新队列中节点的数?占?字节数(含AVPacket.data的??)及其时?。主要?来控制Packet队列的??,我们PacketQueue链表式的队列,在内存充?的条件下我们可以?限插?packet,如果我们要控制队列??,则需要通过其变量size、duration、nb_packets三者单?或者综合去约束队列的节点的数量(实际情况也是 要约束),具体在read_thread进?分析。


packet_queue_get()

从队列中取?个节点:

/*
 q :队列
 pkt:输出参数,即MyAVPacketList.pkt。
 block:调?者是否需要在没节点可取的情况下阻塞等待。
 serial:输出参数,即MyAVPacketList.serial。
 @return <0: aborted;
 =0: no packet;
 >0: has packet
*/
static int packet_queue_get(PacketQueue *q, AVPacket *pkt, int block, int *serial)
{
    MyAVPacketList *pkt1;
    int ret;
   // 加锁
    SDL_LockMutex(q->mutex);

    for (;;) {
      //如果已经退出,那就直接返回
        if (q->abort_request) {
            ret = -1;
            break;
        }
//从队头拿数据,MyAVPacketList *pkt1
        pkt1 = q->first_pkt;
      //队列中有数据
        if (pkt1) {
          //队头移到第?个节点
            q->first_pkt = pkt1->next;
            if (!q->first_pkt)
                q->last_pkt = NULL;
          //节点数减1
            q->nb_packets--;
          //cache??减去 ?个节点的大小
            q->size -= pkt1->pkt.size + sizeof(*pkt1);
          //总时长减去一个pkt的时长的大小
            q->duration -= pkt1->pkt.duration;
            *pkt = pkt1->pkt;
           //如果需要输出serial,把serial输出,输出相应的播放序列
            if (serial)
                *serial = pkt1->serial;
          //释放节点内存,只是释放节点,?不是释放AVPacket
            av_free(pkt1);
            ret = 1;
            break;
        } else if (!block) {
          //这里表示当队列中没有数据,?阻塞调?
            ret = 0;
            break;
        } else {
          //这里表示队列中没有数据,阻塞调?
          //一直等到条件满足
          //这?没有break。for循环的另?个作?是在条件变量满?后重复上述代码取出节点
            SDL_CondWait(q->cond, q->mutex);
        }
    }
  // 释放锁
    SDL_UnlockMutex(q->mutex);
    return ret;
}

总的来说,该函数做了如下一些事情:

(1)加锁

(2)进?for循环,从队列头部取出数据,取出数据后也要减小包的数量,总时长减少,总缓存减少,如果需要退出for循环,则break;当没有数据可读且block为1时则等待。给输出参数赋值:就是MyAVPacketList的成员传递给输出参数pkt和serial。

当返回值ret = -1时,终?获取packet。

当返回值ret = 0时,没有读取到packet。

当返回值ret = 1时,获取到了packet。

(3)释放锁,释放节点内存:释放放?队列时申请的节点内存。(注意是节点内存?不是AVPacket的数据的内存)。

packet_queue_put_nullpacket()

放?“空包”(nullpacket)主要是在放?“空包”(nullpacket),代表流的结束。?般在媒体数据读取完成的时候放?空包。放?空包?的是为了冲刷解码器,将编码器??所有frame都读取出来。

static int packet_queue_put_nullpacket(PacketQueue *q, int stream_index)
{
    AVPacket pkt1, *pkt = &pkt1;
    av_init_packet(pkt);
    pkt->data = NULL;
    pkt->size = 0;
    pkt->stream_index = stream_index;
    return packet_queue_put(q, pkt);
}


packet_queue_flush()

packet_queue_flush?于将packet队列中的所有节点清除包括节点对应的AVPacket。?如?于退出播放和seek播放

(1)退出播放,则要清空packet queue的节点。

(2)seek播放,要清空seek之前缓存的节点数据,以便插?新节点数据

函数主体的for循环是队列遍历遍历过程释放节点和AVPacket(AVpacket对应的数据也被释放掉)。最后将PacketQueue的属性恢复为空队列状态

static void packet_queue_flush(PacketQueue *q)
{
    MyAVPacketList *pkt, *pkt1;

    SDL_LockMutex(q->mutex);
    for (pkt = q->first_pkt; pkt; pkt = pkt1) {
        pkt1 = pkt->next;
      // 释放AVPacket的数据
        av_packet_unref(&pkt->pkt);
        av_freep(&pkt);
    }
    q->last_pkt = NULL;
    q->first_pkt = NULL;
    q->nb_packets = 0;
    q->size = 0;
    q->duration = 0;
    SDL_UnlockMutex(q->mutex);
}

PacketQueue总结

前?我们分析了PacketQueue的实现和主要的操作?法,现在总结下两个关键的点:

(1)PacketQueue的内存管理。结构如下:

注意如下:

MyAVPacketList的自身内存是完全由PacketQueue维护的,在put的时候malloc,在get的时候free。

AVPacket分两块:

?部分是AVPacket本身结构体的内存,这部分从MyAVPacketList的定义可以看出是和MyAVPacketList共存亡的,生命周期一样。

另?部分是AVPacket字段指向的内存(正真的数据区域data),这部分?般通过 av_packet_unref 函数释放。?般情况下,是在get后由调?者负责? av_packet_unref 函数释放特殊的情况是当碰到packet_queue_flush 或put失败时,这时需要队列??处理

(2)serial的变化过程

如上图所示,左边是队头,右边是队尾,从左往右标注了4个节点的serial,以及放?对应节点时queue的serial。可以看到放?flush_pkt的时候后,serial增加了1。假设,现在要从队头取出?个节点,那么取出的节点是serial 1,?PacketQueue?身的queue已经增?到了2,这两个serial的计算,有时候,不太一样。

PacketQueue设计思路:

(1)设计?个多线程安全的队列,保存AVPacket,同时统计队列内已缓存的数据??。(这个统计数据会?来后续设置要缓存的数据量)

(2)引?serial的概念,区别前后数据包是否连续,主要应?于seek或退出操作。

(3)设计了两类特殊的packet——flush_pkt(改变serial)和nullpkt(冲刷解码器,拿出缓存的数据)(类似?于多线程编程的事件模型——往队列中放?flush事件、放?null事件),我们在?频输出、视频输出、播放控制等模块时也会继续对flush_pkt和nullpkt的作?展开分析。


struct Frame 和 FrameQueue队列

这个frame的结构体是一个通用结构体,也就是说音频,视频,字幕,都是可以用。

typedef struct Frame {
  // 指向数据帧
    AVFrame *frame;
  // ?于字幕
    AVSubtitle sub;
  // 播放序列,在seek的操作时serial会变化
    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;
  /*
   对于图像为(enum AVPixelFormat),对于声?则为(enum AVSampleFormat)
  */
    int format;
  //图像的宽??,如果未知或未指定则为0/1。
    AVRational sar;
  // ?来记录该帧是否已经显示过?
    int uploaded;
  // =1则旋转180, = 0则正常播放
    int flip_v;
} Frame;

真正存储解码后?视频数据的结构体为AVFrame,存储字幕则使?AVSubtitle,该Frame的设计是为了?频、视频、字幕帧通?,所以Frame结构体的设计类似AVFrame,部分成员变量只对不同类型有作?,?如sar只对视频有作?。??也包含了serial播放序列(每次seek时都切换serial),sar(图像的宽??(16:9,4:3...),该值来?AVFrame结构体的sample_aspect_ratio变量。


FrameQueue

typedef struct FrameQueue {
  // FRAME_QUEUE_SIZE 最? size, 数字太?时会占??量的内存,需要注意该值的设置
    Frame queue[FRAME_QUEUE_SIZE];
  // 读索引。待播放时读取此帧进?播放,播放后此帧成为上?帧
    int rindex;
  // 写索引
    int windex;
  // 当前总帧数
    int size;
  // 可存储最?帧数
    int max_size;
  // = 1说明要在队列??保持最后? 帧的数据不释放,只在销毁队列的时候才将其真正释放
    int keep_last;
  // 初始化为0,配合keep_last=1 使?
    int rindex_shown;
  // 互斥量
    SDL_mutex *mutex;
  // 条件变量
    SDL_cond *cond;
  // 数据包缓冲队列
    PacketQueue *pktq;
} FrameQueue;

FrameQueue是?个环形缓冲区(ring buffer),是?数组实现的?个FIFO。数组?式的环形缓冲区适合于事先明确了缓冲区的最?容量的情形。

ffplay中创建了三个frame_queue:?频frame_queue,视频frame_queue,字幕frame_queue。每?个frame_queue,?个写端?个读端,写端位于解码线程读端位于播放线程

FrameQueue的设计?PacketQueue复杂,它引?了读取节点,但节点不出队列的操作,仅仅是为了查看作用,读取下?节点也不出队列等等的操作。这些都是数组FIFO特有的功能。

FrameQueue操作提供以下?法:

frame_queue_unref_item:释放Frame??的AVFrame和 AVSubtitle,必须调用者自己去调用。

frame_queue_init:初始化队列。

frame_queue_destory:销毁队列。

frame_queue_signal:发送唤醒信号

frame_queue_peek:获取当前Frame,调?之前先确调?frame_queue_nb_remaining确保有frame可读。

frame_queue_peek_next:获取当前Frame的下?Frame,调?之前先调?frame_queue_nb_remaining确保?少有2 Frame在队列

frame_queue_peek_last:获取上?Frame。

frame_queue_peek_writable:获取?个可写Frame,可以以阻塞或?阻塞?式进?。

frame_queue_peek_readable:获取?个可读Frame,可以以阻塞或?阻塞?式进?。

frame_queue_push:更新写索引,此时Frame才真正?队列,队列节点Frame个数加1。

frame_queue_next:更新读索引,此时Frame才真正出队列,队列节点Frame个数减1,内部调?frame_queue_unref_item是否对应的AVFrame和AVSubtitle,需要删除相关内存。

frame_queue_nb_remaining:获取队列Frame节点个数。

frame_queue_last_pos:获取最近播放Frame对应数据在媒体?件的位置,主要在seek时使?。


frame_queue_init() :初始化frame队列。

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;
}

队列初始化函数确定了队列??,将为队列中每?个节点的frame( f->queue[i].frame )分配内存。

注意:只分配Frame对象本身,?不关注Frame中的数据缓冲区Frame中的数据缓冲区是AVBuffer,使?引?计数机制。

f->max_size 是队列的??,此处值为16(由FRAME_QUEUE_SIZE定义),实际分配的时候视频为3,?频为9,字幕为16,因为这?存储的是解码后的数据,不宜设置过?,?如视频当为1080p时,如果为YUV420p格式,?帧就有3110400字节。

#define VIDEO_PICTURE_QUEUE_SIZE 3 // 图像帧缓存数量

#define SUBPICTURE_QUEUE_SIZE 16 // 字幕帧缓存数量

#define SAMPLE_QUEUE_SIZE 9 // 采样帧缓存数量

#define FRAME_QUEUE_SIZE FFMAX(SAMPLE_QUEUE_SIZE,FFMAX(VIDEO_PICTURE_QUEUE_SIZE, SUBPICTURE_QUEUE_SIZE))

f->keep_last 是队列中是否保留最后?次播放的帧的标志。 f->keep_last =!!keep_last 是将int取值的keep_last转换为boot取值(0或1)。

frame_queue_destory():销毁

static void frame_queue_destory(FrameQueue *f)
{
    int i;
    for (i = 0; i < f->max_size; i++) {
        Frame *vp = &f->queue[i];
      // 释放对vp->frame中的数据缓冲区的引?,注意不是释放frame对象本身
        frame_queue_unref_item(vp);
      // 释放vp->frame对象本身
        av_frame_free(&vp->frame);
    }
    SDL_DestroyMutex(f->mutex);
    SDL_DestroyCond(f->cond);
}

队列销毁函数对队列中的每个节点作了如下处理:

(1)frame_queue_unref_item(vp) 释放本队列对vp->frame中AVBuffer的引?

(2)av_frame_free(&vp->frame)释放vp->frame对象本身。


frame_queue_peek_writable():获取可写Frame。

frame_queue_push():?队列。

FrameQueue写队列的步骤和PacketQueue不同,分了3步进?:

(1)调?frame_queue_peek_writable获取可写的Frame,如果队列已满则等待

(2)获取到Frame后,设置Frame的成员变量。

(3)再调?frame_queue_push更新队列的写索引,真正将Frame?队列。

主要是通过以下2种方法。

Frame *frame_queue_peek_writable(FrameQueue *f); // 获取可写帧

void frame_queue_push(FrameQueue *f); // 更新写索引


看?下写队列的?法:

static int queue_picture(VideoState *is, AVFrame *src_frame, double pts, double duration, int64_t pos, int serial)
{
    Frame *vp;

#if defined(DEBUG_SYNC)
    printf("frame_type=%c pts=%0.3f\n",
           av_get_picture_type_char(src_frame->pict_type), pts);
#endif
 // 检测队列是否有可写空间
    if (!(vp = frame_queue_peek_writable(&is->pictq)))
        return -1;
//设置相关变量
    vp->sar = src_frame->sample_aspect_ratio;
    vp->uploaded = 0;

    vp->width = src_frame->width;
    vp->height = src_frame->height;
    vp->format = src_frame->format;

    vp->pts = pts;
    vp->duration = duration;
    vp->pos = pos;
    vp->serial = serial;

    set_default_window_size(vp->width, vp->height, vp->sar);
   // 将src中所有数据拷?到dst 中,并复位src。
    av_frame_move_ref(vp->frame, src_frame);
   // 更新写索引位置,正真的入队列。
    frame_queue_push(&is->pictq);
    return 0;
}

详细说明如下:

(1)frame_queue_peek_writable(&is->pictq) :向队列尾部申请?个可写的帧空间,若队列已满?空间可写,则等待(由SDL_cond *cond控制,由frame_queue_next或frame_queue_signal触发唤醒)

(2)av_frame_move_ref(vp->frame, src_frame) :将src_frame中所有数据拷?到vp->frame,复位src_frame,vp->frame中AVBuffer使?引?计数机制不会执?AVBuffer的拷?动作修改指针指向值。注意:为避免内存泄漏,在 av_frame_move_ref(dst, src) 之前应先调? av_frame_unref(dst)(如果是自己做) ,这?没有调?,是因为frame_queue在删除?个节点时,已经释放了frame及frame中的AVBuffer。

(3)frame_queue_push(&is->pictq) 此步仅将frame_queue中的写索引加1,实际的数据写?在此步之前已经完成。

frame_queue_peek_writable:获取可写Frame指针。

// 获取可写指针
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];
}

向队列尾部申请?个可写的帧空间,若?空间可写,则等待。

注意:在等待时如果播放器需要退出则将abort_request = 1,所以这个while循环要一直去检测判断,那frame_queue_peek_writable函数可以知道是正常frame可写唤醒,还是其他唤醒。

frame_queue_push():插入队列。

// 更新写索引
static void frame_queue_push(FrameQueue *f)
{
    if (++f->windex == f->max_size)
        f->windex = 0;
    SDL_LockMutex(f->mutex);
    f->size++;
  // 当frame_queue_peek_readable在等待时 则可以唤醒。因为这时候有了数据。
    SDL_CondSignal(f->cond);
    SDL_UnlockMutex(f->mutex);
}

向队列尾部压??帧,只更新计数f->size++与写指针++f->windex,因此调?此函数前应将帧数据写?队列相应位置。SDL_CondSignal(f->cond);可以唤醒读frame_queue_peek_readable。


frame_queue_peek_readable():获取可读Frame。

frame_queue_next():出队列

写队列中,应?程序写??个新帧后通常总是将写索引加1。

注意:读队列中,“读取”和“更新读索引(同时删除旧帧)”?者是独?的,可以只读取?不更新读索引,也可以只更新读索引(只删除)?不读取(只有更新读索引的时候才真正释放对应的Frame数据),这两者相互不影响。?且读队列引?了是否保留已显示的最后?帧的机制,导致读队列?写队列要复杂很多。

读队列和写队列步骤是类似的,基本步骤如下:

(1)调?frame_queue_peek_readable获取可读Frame。

(2)如果需要更新读索引(出队列该节点)则调?frame_queue_peek_next。

读队列涉及如下函数:

// 获取可读Frame指针(若读空则等待)

Frame *frame_queue_peek_readable(FrameQueue *f);

// 获取当前Frame指针

Frame *frame_queue_peek(FrameQueue *f);

// 获取下?Frame指针,一般都需要先判断,是不是至少有2帧

Frame *frame_queue_peek_next(FrameQueue *f);

// 获取上?Frame指针

Frame *frame_queue_peek_last(FrameQueue *f);

// 更新读索引(同时删除旧frame)

void frame_queue_next(FrameQueue *f);


通过实例看?下读队列的?法:

static void video_refresh(void *opaque, double *remaining_time)
{
    VideoState *is = opaque;
    double time;

    Frame *sp, *sp2;

    if (!is->paused && get_master_sync_type(is) == AV_SYNC_EXTERNAL_CLOCK && is->realtime)
        check_external_clock_speed(is);

    if (!display_disable && is->show_mode != SHOW_MODE_VIDEO && is->audio_st) {
        time = av_gettime_relative() / 1000000.0;
        if (is->force_refresh || is->last_vis_time + rdftspeed < time) {
            video_display(is);
            is->last_vis_time = time;
        }
        *remaining_time = FFMIN(*remaining_time, is->last_vis_time + rdftspeed - time);
    }

    if (is->video_st) {
retry:
      //读队列已经没有了数据,所有帧已显示
        if (frame_queue_nb_remaining(&is->pictq) == 0) {
            // nothing to do, no picture to display in the queue
        } else {
            double last_duration, duration, delay;
            Frame *vp, *lastvp;

            /* dequeue the picture */
          // 上?帧:上次 已显示的帧
            lastvp = frame_queue_peek_last(&is->pictq);
          // 当前帧:当前 待显示的帧
            vp = frame_queue_peek(&is->pictq);

            if (vp->serial != is->videoq.serial) {
              // 出队列,并更新rindex
                frame_queue_next(&is->pictq);
                goto retry;
            }

            if (lastvp->serial != vp->serial)
                is->frame_timer = av_gettime_relative() / 1000000.0;

            if (is->paused)
                goto display;

            /* compute nominal last_duration */
            last_duration = vp_duration(is, lastvp, vp);
            delay = compute_target_delay(last_duration, is);

            time= av_gettime_relative()/1000000.0;
            if (time < is->frame_timer + delay) {
                *remaining_time = FFMIN(is->frame_timer + delay - time, *remaining_time);
                goto display;
            }

            is->frame_timer += delay;
            if (delay > 0 && time - is->frame_timer > AV_SYNC_THRESHOLD_MAX)
                is->frame_timer = time;

            SDL_LockMutex(is->pictq.mutex);
            if (!isnan(vp->pts))
                update_video_pts(is, vp->pts, vp->pos, vp->serial);
            SDL_UnlockMutex(is->pictq.mutex);

            if (frame_queue_nb_remaining(&is->pictq) > 1) {
                Frame *nextvp = frame_queue_peek_next(&is->pictq);
                duration = vp_duration(is, vp, nextvp);
                if(!is->step && (framedrop>0 || (framedrop && get_master_sync_type(is) != AV_SYNC_VIDEO_MASTER)) && time > is->frame_timer + duration){
                    is->frame_drops_late++;
                    frame_queue_next(&is->pictq);
                    goto retry;
                }
            }

            if (is->subtitle_st) {
                    while (frame_queue_nb_remaining(&is->subpq) > 0) {
                        sp = frame_queue_peek(&is->subpq);

                        if (frame_queue_nb_remaining(&is->subpq) > 1)
                            sp2 = frame_queue_peek_next(&is->subpq);
                        else
                            sp2 = NULL;

                        if (sp->serial != is->subtitleq.serial
                                || (is->vidclk.pts > (sp->pts + ((float) sp->sub.end_display_time / 1000)))
                                || (sp2 && is->vidclk.pts > (sp2->pts + ((float) sp2->sub.start_display_time / 1000))))
                        {
                            if (sp->uploaded) {
                                int i;
                                for (i = 0; i < sp->sub.num_rects; i++) {
                                    AVSubtitleRect *sub_rect = sp->sub.rects[i];
                                    uint8_t *pixels;
                                    int pitch, j;

                                    if (!SDL_LockTexture(is->sub_texture, (SDL_Rect *)sub_rect, (void **)&pixels, &pitch)) {
                                        for (j = 0; j < sub_rect->h; j++, pixels += pitch)
                                            memset(pixels, 0, sub_rect->w << 2);
                                        SDL_UnlockTexture(is->sub_texture);
                                    }
                                }
                            }
                            frame_queue_next(&is->subpq);
                        } else {
                            break;
                        }
                    }
            }

            frame_queue_next(&is->pictq);
            is->force_refresh = 1;

            if (is->step && !is->paused)
                stream_toggle_pause(is);
        }
display:
        /* display picture */
        if (!display_disable && is->force_refresh && is->show_mode == SHOW_MODE_VIDEO && is->pictq.rindex_shown)
            video_display(is);
    }
    is->force_refresh = 0;
    if (show_status) {
        static int64_t last_time;
        int64_t cur_time;
        int aqsize, vqsize, sqsize;
        double av_diff;

        cur_time = av_gettime_relative();
        if (!last_time || (cur_time - last_time) >= 30000) {
            aqsize = 0;
            vqsize = 0;
            sqsize = 0;
            if (is->audio_st)
                aqsize = is->audioq.size;
            if (is->video_st)
                vqsize = is->videoq.size;
            if (is->subtitle_st)
                sqsize = is->subtitleq.size;
            av_diff = 0;
            if (is->audio_st && is->video_st)
                av_diff = get_clock(&is->audclk) - get_clock(&is->vidclk);
            else if (is->video_st)
                av_diff = get_master_clock(is) - get_clock(&is->vidclk);
            else if (is->audio_st)
                av_diff = get_master_clock(is) - get_clock(&is->audclk);
            av_log(NULL, AV_LOG_INFO,
                   "%7.2f %s:%7.3f fd=%4d aq=%5dKB vq=%5dKB sq=%5dB f=%"PRId64"/%"PRId64"   \r",
                   get_master_clock(is),
                   (is->audio_st && is->video_st) ? "A-V" : (is->video_st ? "M-V" : (is->audio_st ? "M-A" : "   ")),
                   av_diff,
                   is->frame_drops_early + is->frame_drops_late,
                   aqsize / 1024,
                   vqsize / 1024,
                   sqsize,
                   is->video_st ? is->viddec.avctx->pts_correction_num_faulty_dts : 0,
                   is->video_st ? is->viddec.avctx->pts_correction_num_faulty_pts : 0);
            fflush(stdout);
            last_time = cur_time;
        }
    }
}

记lastvp为上?次已播放的帧,vp为本次待播放的帧,下图中?框中的数字表示显示序列中帧的序号:

如下图:灰色表示已经显示完了,红色表示正在显示,绿色表示下一帧即将显示。rindex和windex是读写索引。下面T0和T1表示两个完全不同时刻。

在启?keep_last机制后,rindex_shown值总是为1,rindex_shown确保最后播放的?帧保留在队列中

假设某次进? video_refresh() 的时刻为T0,下次进?的时刻为T1。在T0时刻,读队列的步骤如下:

(1)rindex表示上?次播放的帧lastvp,本次调? video_refresh() 中,lastvp会被删除,rindex会加1,即是当调?frame_queue_next删除的是lastvp(删除的是上一帧),?不是当前的vp(不是当前帧),当前的vp转为lastvp。

(2)rindex+rindex_shown表示本次待播放的帧vp,本次调? video_refresh() 中,vp会被读出播放图中已播放的帧是灰??框,本次待播放的帧是红??框,其他未播放的帧是绿??框,队列中空位置为???框。

(3)rindex+rindex_shown+1表示下?帧nextvp。

frame_queue_nb_remaining():获取队列中剩余未读的size。

/* return the number of undisplayed frames in the queue */
static int frame_queue_nb_remaining(FrameQueue *f) 
{ 
   return f->size - f->rindex_shown; 
}


rindex_shown为1时,队列中总是保留了最后?帧lastvp(灰??框)。需要注意的时候rindex_shown的值就是0或1,不存在变为2,3等的可能。在计算队列当前Frame数量是不包含lastvp。

rindex_shown的引?增加了读队列操作的理解难度。?多数读操作函数都会?到这个变量。通过 FrameQueue.keep_last 和 FrameQueue.rindex_shown 两个变量实现了保留最后?次播放帧的机制。

注意:是否启?keep_last机制是由全局变量 keep_last (就是为了区分上一帧和当前帧的区别)值决定的,在队列初始化函数frame_queue_init() 中有 f->keep_last = !!keep_last; ,?在更新读指针函数frame_queue_next() 中如果启?keep_last机制,则 f->rindex_shown 值为1

具体分析下 frame_queue_next() 函数:

/* 释放当前frame,并更新读索引rindex,
 * 当keep_last为1, rindex_show为0时不去更新rindex,也不释放当前frame ,保证最后一帧永远在队列中*/
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);
}

主要步骤:

(1)在启?keeplast时,如果rindex_shown为0则将其设置为1,并返回。此时并不会更新读索引(在最后并不会删除)。也就是说keeplast机制实质上也会占?着队列Frame的size,当调?frame_queue_nb_remaining()获取size时并不能将其计算?size。

(2)释放Frame对应的数据(?如AVFrame的数据),但不释放Frame本身,就是这个节点依然存在。

(3)更新读索引

(4)释放唤醒信号,以唤醒正在等待写?的线程。


frame_queue_peek_readable()源码实现。

队列头部读取?帧(vp),只读取不删除若?帧可读则等待。这个函数和 frame_queue_peek() 的区别仅仅是多了不可读时等待的操作。

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];
}


frame_queue_peek():获取当前帧

 /* 获取队列当前Frame, 在调?该函数前先调?frame_queue_nb_remaining确保有frame 可读 */
static Frame *frame_queue_peek(FrameQueue *f)
{ 
  return &f->queue[(f->rindex + f->rindex_shown) % f->max_size]; 
}

frame_queue_peek_next():获取下?帧

/* 获取当前Frame的下?Frame, 此时要确保queue???少有2个Frame */ 
 static Frame *frame_queue_peek_next(FrameQueue *f) 
{ 
  return &f->queue[(f->rindex + f->rindex_shown + 1) % f->max_size] ; 
}

frame_queue_peek_last():获取上?帧

/* 获取last Frame: 
* 当rindex_shown=0时,和frame_queue_peek效果?样 
* 注意:当rindex_shown=1时,读取的是已经显示过的frame 
*/ 
static Frame *frame_queue_peek_last(FrameQueue *f) 
 { 
   return &f->queue[f->rindex]; 
  }

struct AudioParams:?频参数

typedef struct AudioParams {
  // 采样率
    int freq;
  // 通道数
    int channels;
  // 通道布局,?如2.1声道,5.1声道等
    int64_t channel_layout;
  // ?频采样格式,?如AV_SAMPLE_FM T_S16表示为有符号16bit深度,交错排列模式。
    enum AVSampleFormat fmt;
  // ?个采样单元占?的字节数(?如2通道时,则左右通道各采样?次合成?个采样单元)
    int frame_size;
  // ?秒时间的字节数,?如采样率48Khz,2 channel,16bit,则?秒48000*2*16/8=192000
    int bytes_per_sec;
} AudioParams;

struct Decoder :解码器

typedef struct Decoder {
    AVPacket pkt;
  // 数据包队列
    PacketQueue *queue;
  // 解码器上下?
    AVCodecContext *avctx;
  // 包序列
    int pkt_serial;
  // =0,解码器处于?作状态
  //=?0,解码器处于空闲状态
    int finished;
  // =0,解码器处于异常状态,需要考虑重置解码 器;
  //=1,解码器处于正常状态。
    int packet_pending;
  // 检查到packet队列空时发送 signal缓 存read_thread读取数据
    SDL_cond *empty_queue_cond;
  // 初始化时是stream的start time
    int64_t start_pts;
  // 初始化时是stream的time_base
    AVRational start_pts_tb;
  // 记录最近?次解码后的frame的pts,当 解出来的部分帧没有有效的pts时则使?next_pts进?推算
    int64_t next_pts;
  // next_pts的单位
    AVRational next_pts_tb;
  // 线程句柄
    SDL_Thread *decoder_tid;
} Decoder;

本篇文章就分享到这里,后面会有文章再次分析,欢迎关注,点赞,收藏,转发。也欢迎在评论区交流。

相关推荐

为何越来越多的编程语言使用JSON(为什么编程)

JSON是JavascriptObjectNotation的缩写,意思是Javascript对象表示法,是一种易于人类阅读和对编程友好的文本数据传递方法,是JavaScript语言规范定义的一个子...

何时在数据库中使用 JSON(数据库用json格式存储)

在本文中,您将了解何时应考虑将JSON数据类型添加到表中以及何时应避免使用它们。每天?分享?最新?软件?开发?,Devops,敏捷?,测试?以及?项目?管理?最新?,最热门?的?文章?,每天?花?...

MySQL 从零开始:05 数据类型(mysql数据类型有哪些,并举例)

前面的讲解中已经接触到了表的创建,表的创建是对字段的声明,比如:上述语句声明了字段的名称、类型、所占空间、默认值和是否可以为空等信息。其中的int、varchar、char和decimal都...

JSON对象花样进阶(json格式对象)

一、引言在现代Web开发中,JSON(JavaScriptObjectNotation)已经成为数据交换的标准格式。无论是从前端向后端发送数据,还是从后端接收数据,JSON都是不可或缺的一部分。...

深入理解 JSON 和 Form-data(json和formdata提交区别)

在讨论现代网络开发与API设计的语境下,理解客户端和服务器间如何有效且可靠地交换数据变得尤为关键。这里,特别值得关注的是两种主流数据格式:...

JSON 语法(json 语法 priority)

JSON语法是JavaScript语法的子集。JSON语法规则JSON语法是JavaScript对象表示法语法的子集。数据在名称/值对中数据由逗号分隔花括号保存对象方括号保存数组JS...

JSON语法详解(json的语法规则)

JSON语法规则JSON语法是JavaScript对象表示法语法的子集。数据在名称/值对中数据由逗号分隔大括号保存对象中括号保存数组注意:json的key是字符串,且必须是双引号,不能是单引号...

MySQL JSON数据类型操作(mysql的json)

概述mysql自5.7.8版本开始,就支持了json结构的数据存储和查询,这表明了mysql也在不断的学习和增加nosql数据库的有点。但mysql毕竟是关系型数据库,在处理json这种非结构化的数据...

JSON的数据模式(json数据格式示例)

像XML模式一样,JSON数据格式也有Schema,这是一个基于JSON格式的规范。JSON模式也以JSON格式编写。它用于验证JSON数据。JSON模式示例以下代码显示了基本的JSON模式。{"...

前端学习——JSON格式详解(后端json格式)

JSON(JavaScriptObjectNotation)是一种轻量级的数据交换格式。易于人阅读和编写。同时也易于机器解析和生成。它基于JavaScriptProgrammingLa...

什么是 JSON:详解 JSON 及其优势(什么叫json)

现在程序员还有谁不知道JSON吗?无论对于前端还是后端,JSON都是一种常见的数据格式。那么JSON到底是什么呢?JSON的定义...

PostgreSQL JSON 类型:处理结构化数据

PostgreSQL提供JSON类型,以存储结构化数据。JSON是一种开放的数据格式,可用于存储各种类型的值。什么是JSON类型?JSON类型表示JSON(JavaScriptO...

JavaScript:JSON、三种包装类(javascript 包)

JOSN:我们希望可以将一个对象在不同的语言中进行传递,以达到通信的目的,最佳方式就是将一个对象转换为字符串的形式JSON(JavaScriptObjectNotation)-JS的对象表示法...

Python数据分析 只要1分钟 教你玩转JSON 全程干货

Json简介:Json,全名JavaScriptObjectNotation,JSON(JavaScriptObjectNotation(记号、标记))是一种轻量级的数据交换格式。它基于J...

比较一下JSON与XML两种数据格式?(json和xml哪个好)

JSON(JavaScriptObjectNotation)和XML(eXtensibleMarkupLanguage)是在日常开发中比较常用的两种数据格式,它们主要的作用就是用来进行数据的传...

取消回复欢迎 发表评论:

请填写验证码