当前位置: 首页 > news >正文

ffplay数据结构分析

struct VideoState 播放器封装

typedef struct VideoState {SDL_Thread *read_tid;			// 读线程句柄AVInputFormat *iformat;			// 指向demuxerint abort_request;				// =1时请求退出播放int force_refresh;				// =1时刷新画面,请求立即刷新画面的意思int paused;						// =1时暂停,=0时播放int last_paused;				// 暂存 “暂停”/“播放” 状态int queue_attachments_req;int seek_req;					// 标识一次seek请求int seek_flags;					// seek标志,诸如AVSEEK_FLAG_BYTE等int64_t seek_pos;				// 请求seek的目标位置(当前位置+增量)int64_t seek_rel;				// 本次seek的位置增量int read_pause_return;AVFormatContext *ic;			// iformat上下文int realtime;					// =1为实时流Clock audclk;					// 音频时钟Clock vidclk;					// 视频时钟Clock extclk;					// 外部时钟FrameQueue pictq;				// 视频Frame队列FrameQueue subpq;				// 字幕Frame队列FrameQueue sampq;				// 采样Frame队列Decoder auddec;					// 音频解码器Decoder viddec;					// 视频解码器Decoder subdec;					// 字幕解码器int audio_stream;				// 音频流索引int av_sync_type;				// 音视频同步类型,默认audio masterdouble audio_clock;				// 当前音频帧的PTS+当前帧Durationint audio_clock_serial;			// 播放序列,seek可改变此值// 以下4个参数 非audio master同步方式使用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;				// 音频流PacketQueue audioq;				// 音频packet队列int audio_hw_buf_size;			// SDL音频缓冲区大小(字节为单位)/*指向待播放的一帧音频数据,指向的数据区将被拷入SDL音频缓冲区。如果经过重采样则指向audio_buf1否则指向frame中的音频*/uint8_t *audio_buf;				// 指向需要重采样的数据uint8_t *audio_buf1;			// 指向重采样后的数据unsigned int audio_buf_size;	// 待播放的一帧音频数据(audio_buf指向)大小unsigned int audio_buf1_size;	// 申请到的音频缓冲区audio_buf1的实际尺寸int audio_buf_index;			// 更新拷贝位置,当前音频帧中已拷入SDL// 音频缓冲区的位置索引(指向第一个待拷贝字节)/*当前音频帧中尚未拷入SDL音频缓冲区的数据量audio_buf_size = audio_buf_index + audio_write_buf_size*/int audio_write_buf_size;int audio_volume;				// 音量int muted;						// =1静音,=0则正常struct AudioParams audio_src;	// 音频frame的参数
#if CONFIG_AVFILTERstruct AudioParams audio_filter_src;
#endifstruct AudioParams audio_tgt;	// SDL支持的音频参数,重采样转换:audio_src->audio_tgtstruct SwrContext *swr_ctx;		// 音频重采样contextint frame_drops_early;			// 丢弃视频packet计数int frame_drops_late;			// 丢弃视频frame计数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;			// 字幕流PacketQueue subtitleq;			// 字幕packet队列double frame_timer;				// 记录最后一帧播放的时刻double frame_last_returned_time;double frame_last_filter_delay;int video_stream;				// 视频流索引AVStream *video_st;				// 视频流PacketQueue videoq;				// 视频packet队列double max_frame_duration;      // 一帧最大间隔struct SwsContext *img_convert_ctx;	// 视频尺寸格式变换struct SwsContext *sub_convert_ctx; // 字幕尺寸格式变换int eof;							// 是否读取结束char *filename;						// 文件名int width, height, xleft, ytop;		// 宽 高 x起始坐标 y起始坐标int step;							// =1步进播放模式 =0其他模式#if CONFIG_AVFILTERint vfilter_idx;AVFilterContext *in_video_filter;   // the first filter in the video chainAVFilterContext *out_video_filter;  // the last filter in the video chainAVFilterContext *in_audio_filter;   // the first filter in the audio chainAVFilterContext *out_audio_filter;  // the last filter in the audio chainAVFilterGraph *agraph;              // audio filter graph
#endif// 保存最近的相应audio video subtitle流的steam indexint last_video_stream, last_audio_stream, last_subtitle_stream;// 当读取数据队列满了后进入休眠时,可以通过该condition唤醒读现成SDL_cond *continue_read_thread;
} VideoState;

struct Clock 时钟封装

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

struct MyAVPacketList 和 PacketQueue 队列

ffplay 用 PacketQueue 保存解封装后的数据,即保存 AVPacket

ffplay 首先定义了一个结构体 MyAVPacketList

typedef struct MyAVPacketList {AVPacket pkt;					// 解封装后的数据struct MyAVPacketList *next;	// 下一个节点int serial;						// 播放序列
} MyAVPacketList;

可以理解为是队列的一个节点。可以通过 next 字段访问下一个节点。

serial字段主要⽤于标记当前节点的播放序列号,ffplay中多处⽤到serial的概念,主要⽤来区分是否连续

数据,每做⼀次seek,该serial都会做+1的递增,以区分不同的播放序列。serial字段在我们ffplay的分析

中应⽤⾮常⼴泛,谨记他是⽤来区分数据否连续先。

接着定义另一个结构体 PacketQueue:

typedef struct PacketQueue {MyAVPacketList *first_pkt, *last_pkt; // 队首 队尾指针int nb_packets; // 包数量,也就是队列元素数量int size; // 队列所有元素的数据大小总和int64_t duration; // 队列所有元素的数据播放持续时间int abort_request; // 用户退出请求标志int serial; // 播放序列号和上文作用相同SDL_mutex *mutex; // 用于维持PacketQueue的多线程安全SDL_cond *cond; // 用于读,写线程互相通知
} PacketQueue;

该结构体内定义了“队列”⾃身的属性。上⾯的注释对每个字段作了简单的介绍,这⾥也看到了serial字段, MyAVPacketList的serial字段的赋值来⾃PacketQueue的serial,每个PacketQueue的serial是独⽴的。

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

<font style="color:rgb(38,38,38);">PacketQueue</font>操作提供以下方法:

  • 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:

/* packet queue handling */
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);}q->abort_request = 1; // 在packete_queue_start和packet_queue_abort时修改该值return 0;
}

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);q->abort_request = 0;packet_queue_put_private(q, &flush_pkt); // 这里插入了一个flush_pkt目的是什么SDL_UnlockMutex(q->mutex);
}

flush_pkt 的定义是 static AVPacket flush_pkt,是一个特殊的 packet,主要用来作为非连续的两端数据“分界”的标记:

  • 插入 flush_pkt触发 PacketQueue 其对应的 serial,加 1 操作
  • 触发解码器清空自身缓存 avcodec_flush_buffers(),以备新序列的数据进行刷新解码

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);				// 放入失败,释放AVPacketreturn 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;pkt1->pkt = *pkt; // 拷贝AVPacket(浅拷贝,AVPacket.data等内存并没有拷贝)pkt1->next = NULL;if (pkt == &flush_pkt) // 如果放入的是flush_pkt,需要增加队列的播放序列号,区分不同两段数据q->serial++;pkt1->serial = q->serial; // 用队列序列号标记节点if (!q->last_pkt)q->first_pkt = pkt1;elseq->last_pkt->next = pkt1;q->last_pkt = pkt1;// 队列属性操作:增加节点数,cache大小,cache总时长,用来控制队列的大小q->nb_packets++;q->size += pkt1->pkt.size + sizeof(*pkt1);q->duration += pkt1->pkt.duration;/* XXX: should duplicate packet data in DV case */// 发出信号,表明当前队列中有数据了,通知等待线程中的读线程可以读取数据了SDL_CondSignal(q->cond);return 0;
}

对于 packet_queue_put_private 主要完成三件事:

  • 计算serial。serial标记了这个节点内的数据是何时的。⼀般情况下新增节点与上⼀个节点的serial是⼀ 样的,但当队列中加⼊⼀个flush_pkt后,后续节点的serial会⽐之前⼤1,⽤来区别不同播放序列的 packet。
  • 节点⼊队列操作。
  • 队列属性操作。更新队列中节点的数⽬、占⽤字节数(含AVPacket.data的⼤⼩)及其时⻓。主要⽤来 控制Packet队列的⼤⼩,我们PacketQueue链表式的队列,在内存充⾜的条件下我们可以⽆限put⼊ packet,如果我们要控制队列⼤⼩,则需要通过其变量size、duration、nb_packets三者单⼀或者综 合去约束队列的节点的数量,具体在read_thread进⾏分析。

packet_queue_get()

从队列中取一个节点:

/*** @brief packet_queue_get* @param q 队列* @param pkt 输出参数,即MyAVPacketList.pkt* @param block 调⽤者是否需要在没节点可取的情况下阻塞等待* @param 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;}pkt1 = q->first_pkt;if (pkt1) { // 队列中有数据q->first_pkt = pkt1->next; // 队头移动到第二个节点if (!q->first_pkt)q->last_pkt = NULL;q->nb_packets--;	// 节点-1q->size -= pkt1->pkt.size + sizeof(*pkt1); // cache大小扣一个节点q->duration -= pkt1->pkt.duration; // 时长扣掉一个节点*pkt = pkt1->pkt;if (serial)*serial = pkt1->serial;av_free(pkt1); // 释放节点内存,只释放节点,不释放AVPacketret = 1;break;} else if (!block) { //队列中没有数据,且⾮阻塞调⽤ret = 0;break;} else { // 队列中没有数据,阻塞调用//这⾥没有break。for循环的另⼀个作⽤是在条件变量满⾜后重复上述代码取节点SDL_CondWait(q->cond, q->mutex);}}SDL_UnlockMutex(q->mutex);return ret;
}

该函数整体流程:

  • 加锁
  • 进⼊for循环,如果需要退出for循环,则break;当没有数据可读且block为1时则等待
    • ret = -1 终⽌获取packet
    • ret = 0 没有读取到packet
    • ret = 1 获取到了packet
  • 释放锁

如果有取到数据,主要分3个步骤:

  1. 队列操作:出队列操作; nb_packets相应-1; duration 的也相应减少, size也相应占⽤的字节
    ⼤⼩(pkt1->pkt.size + sizeof(*pkt1))
  2. 给输出参数赋值:就是MyAVPacketList的成员传递给输出参数pkt和serial
  3. 释放节点内存:释放放⼊队列时申请的节点内存(注意是节点内存⽽不是AVPacket的数据的内存)

packet_queue_put_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播放:

  • 退出播放,则要清空packet queue的节点
  • seek播放,要清空seek之前缓存的节点数据,以便插⼊新节点数据
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;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);
}

函数主体的for循环是队列遍历,遍历过程释放节点和AVPacket(AVpacket对应的数据也被释放掉)。最后

将PacketQueue的属性恢复为空队列状态。

PacketQueue 总结

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

第一部分:内存管理

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

AVPacket分两块:

  • ⼀部分是AVPacket结构体的内存,这部分从MyAVPacketList的定义可以看出是和MyAVPacketList 共存亡的。
  • 另⼀部分是AVPacket字段指向的内存,这部分⼀般通过 av_packet_unref 函数释放。⼀般情况 下,是在get后由调⽤者负责⽤ av_packet_unref 函数释放。特殊的情况是当碰到 packet_queue_flush 或put失败时,这时需要队列⾃⼰处理。

第二部分:serial 的变化过程

如上图所示,左边是队头,右边是队尾,从左往右标注了4个节点的serial,以及放⼊对应节点时queue的

serial。可以看到放⼊flush_pkt的时候后,serial增加了1。

假设,现在要从队头取出⼀个节点,那么取出的节点是serial 1,⽽PacketQueue⾃身的queue已经增⻓到了 2。

PacketQueue设计思路:

  1. 设计⼀个多线程安全的队列,保存AVPacket,同时统计队列内已缓存的数据⼤⼩。(这个统计数据会⽤来后续设置要缓存的数据量)
  2. 引⼊serial的概念,区别前后数据包是否连续,主要应⽤于seek操作。
  3. 设计了两类特殊的packet——flush_pkt和nullpkt(类似⽤于多线程编程的事件模型——往队列中放⼊ flush事件、放⼊null事件),我们在⾳频输出、视频输出、播放控制等模块时也会继续对flush_pkt和 nullpkt的作⽤展开分析。

struct Frame 和 FrameQueue 队列

struct Frame

typedef struct Frame {AVFrame *frame;			// 指向数据帧AVSubtitle sub;			// 用于字幕int serial;				// 播放序列,在seek的操作时serial会变化double pts;				// 时间戳(秒)double duration;		// 帧持续时间(秒)int64_t pos;			// 帧在输入文件中的字节位置int width;				// 宽int height;				// 高int format;				// 对于图像:enum AVPixelFormat// 对于音频:enum AVSampleFormatAVRational sar;			// 图像宽高比,默认0/1int uploaded;			// 用来记录帧是否已经显示过int flip_v;				// =1则旋转180 =0正常播放
} Frame;

真正存储解码后⾳视频数据的结构体为AVFrame ,存储字幕则使⽤AVSubtitle,该Frame的设计是为了⾳ 频、视频、字幕帧通⽤,所以Frame结构体的设计类似AVFrame,部分成员变量只对不同类型有作⽤,⽐如sar只对视频有作⽤。

⾥⾯也包含了serial播放序列(每次seek时都切换serial),sar(图像的宽⾼⽐(16:9,4:3…),该值来

⾃AVFrame结构体的sample_aspect_ratio变量)。

struct FrameQueue

typedef struct FrameQueue {Frame queue[FRAME_QUEUE_SIZE];	// FRAME_QUEUE_SIZE最大sizeint rindex;						// 读索引,待播放时读取此帧播放,播放后此帧成为上一帧int windex;						// 写索引int size;						// 当前总帧数int max_size;					// 可存储的最大帧数int keep_last;					// =1说明要在队列保持最后一帧数据不释放,只销毁队列时才真正释放int rindex_shown;				// 初始化为0 配置keep_last=1使用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复杂,引⼊了读取节点但节点不出队列的操作、读取下⼀节点也不

出队列等等的操作,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()初始化

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())) // 分配AVFrame结构体return AVERROR(ENOMEM);return 0;
}

队列初始化函数确定了队列⼤⼩,将为队列中每⼀个节点的frame(<font style="color:rgb(38,38,38);">f->queue[i].frame</font>)分配内

存,注意只是分配Frame对象本身,⽽不关注Frame中的数据缓冲区。Frame中的数据缓冲区是

AVBuffer,使⽤引⽤计数机制。

<font style="color:rgb(38,38,38);">f->max_size</font> 是队列的⼤⼩,此处值为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))

<font style="color:rgb(38,38,38);">f->keep_last</font>是队列中是否保留最后⼀次播放的帧的标志。<font style="color:rgb(38,38,38);">f->keep_last = !!keep_last</font>是将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->frameav_frame_free(&vp->frame);}SDL_DestroyMutex(f->mutex);SDL_DestroyCond(f->cond);
}

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

  1. <font style="color:rgb(38,38,38);">frame_queue_unref_item(vp)</font>释放本队列对vp->frame中AVBuffer的引⽤
  2. <font style="color:rgb(38,38,38);">av_frame_free(&vp->frame)</font>释放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⼊队列
// 获取可写帧
/*
向队列尾部申请⼀个可写的帧空间,若⽆空间可写,则等待。
这⾥最需要体会到的是abort_request的使⽤,在等待时如果播放器需要退出则将abort_request = 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];
}
// 更新写索引
static void frame_queue_push(FrameQueue *f)
{if (++f->windex == f->max_size)f->windex = 0;SDL_LockMutex(f->mutex); // 当frame_queue_peek_readable等待时可以唤醒f->size++;SDL_CondSignal(f->cond);SDL_UnlockMutex(f->mutex);
}
/*
向队列尾部压⼊⼀帧,只更新计数与写指针,因此调⽤此函数前应将帧数据写⼊队列相应位
置。SDL_CondSignal(f->cond);可以唤醒读frame_queue_peek_readable。
*/

通过具体场景看写队列的用法:

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// frame_queue_peek_writable监测队列是否有可写空间if (!(vp = frame_queue_peek_writable(&is->pictq)))return -1; // 队列满了返回-1// 执行到这一步说明已经获取了可写入的Framevp->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中,并复位srcav_frame_move_ref(vp->frame, src_frame);// 插入,更新写索引位置frame_queue_push(&is->pictq);return 0;
}

上⾯⼀段代码是视频解码线程向视频frame_queue中写⼊⼀帧的代码,步骤如下:

  1. <font style="color:rgb(38,38,38);">frame_queue_peek_writable(&is->pictq) </font>向队列尾部申请⼀个可写的帧空间,若队列已满 ⽆空间可写,则等待(由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. <font style="color:rgb(38,38,38);">frame_queue_push(&is->pictq)</font>此步仅将frame_queue中的写索引加1,实际的数据写⼊在此步之前已经完成。

frame_queue_peek_readable()获取可读 Frame

frame_queue_next()出队列

写队列中,应⽤程序写⼊⼀个新帧后通常总是将写索引加1。⽽读队列中,“读取”和“更新读索引(同时删除 旧帧)”⼆者是独⽴的,可以只读取⽽不更新读索引,也可以只更新读索引(只删除)⽽不读取(只有更新读索引的时候才真正释放对应的Frame数据)。⽽且读队列引⼊了是否保留已显示的最后⼀帧的机制,导致读队列⽐写队列要复杂很多。

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

  1. 调⽤frame_queue_peek_readable获取可读Frame;
  2. 如果需要更新读索引(出队列该节点)则调⽤frame_queue_peek_next;

读队列涉及如下函数:

// 获取可读Framezhizhen(读空则等待)
static Frame *frame_queue_peek_readable(FrameQueue *f);
// 获取当前Frame指针
static Frame *frame_queue_peek(FrameQueue *f);
// 获取下一个Frame指针
static Frame *frame_queue_peek_next(FrameQueue *f);
// 获取上一个Frame指针
static Frame *frame_queue_peek_last(FrameQueue *f);
// 更新读索引,删除旧frame
static void frame_queue_next(FrameQueue *f);

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

static void video_refresh(void *opaque, double *remaining_time)
{if (frame_queue_nb_remaining(&is->pictq) == 0) /* 所有帧已显示 */ {} else {double last_duration, duration, delay;Frame *vp, *lastvp;// 上一帧:上一个已显示的帧lastvp = frame_queue_peek_last(&is->pictq);// 当前帧:当前待显示的帧vp = frame_queue_peek(&is->pictq);if (vp->serial != is->videoq.serial) {// 出队列,更新rindexframe_queue_next(&is->pictq);goto retry;}}
}

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

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

假设某次进⼊<font style="color:rgb(38,38,38);">video_refresh()</font>的时刻为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

static int frame_queue_nb_remaining(FrameQueue *f)
{return f->size - f->rindex_shown;
}

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()获取下一帧

// 使用时需要确保queue里面至少两个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()获取上一帧

// 当rindex_shown=0时,和frame_queue_peek效果一样,获取当前帧
// 当rindex_show=1时,读取上一个
static Frame *frame_queue_peek_last(FrameQueue *f)
{return &f->queue[f->rindex];
}

struct AudioParams 音频参数

typedef struct AudioParams {int freq;						// 采样率int channels;					// 通道数int64_t channel_layout;			// 通道布局,比如2.1声道,5.1声道等enum AVSampleFormat fmt;		// 音频采样格式,比如AV_SAMPLE_FMT_S16表示16bit交错排列int frame_size;					// 一个采样单元占用的字节数,比如2通道则左右各采样一次合成一个采样单元int bytes_per_sec;				// ⼀秒时间的字节数,⽐如采样率48Khz,2channel,16bit// 则一秒48000*2*16/8=192000
} AudioParams;

struct Decoder 解码器封装

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

参考资料:https://github.com/0voice


文章转载自:

http://IWrJ6yXc.nLgyq.cn
http://W685zzpE.nLgyq.cn
http://dSzMHY3J.nLgyq.cn
http://s3IMEJDS.nLgyq.cn
http://4uqBWut7.nLgyq.cn
http://ORw4GKTT.nLgyq.cn
http://F67I66s7.nLgyq.cn
http://aSgJFtFt.nLgyq.cn
http://H09u1DKR.nLgyq.cn
http://5nEqmifV.nLgyq.cn
http://MQI170BX.nLgyq.cn
http://we3lKRQi.nLgyq.cn
http://Hytt5tTF.nLgyq.cn
http://eWwp8Gv4.nLgyq.cn
http://G2JKa9ZC.nLgyq.cn
http://RMI7zcsB.nLgyq.cn
http://mDRFFLQf.nLgyq.cn
http://8aBuPFpo.nLgyq.cn
http://pv1qjfAG.nLgyq.cn
http://nkXw8Y8h.nLgyq.cn
http://3TeUafid.nLgyq.cn
http://NU9ppDMQ.nLgyq.cn
http://XHMoDjVH.nLgyq.cn
http://6SttpjN0.nLgyq.cn
http://3hP7WCjn.nLgyq.cn
http://2d9SUSlx.nLgyq.cn
http://CBJCGdb4.nLgyq.cn
http://iGOtmjAk.nLgyq.cn
http://6Pso9nVY.nLgyq.cn
http://LjR5BLwc.nLgyq.cn
http://www.dtcms.com/a/383286.html

相关文章:

  • 我爱学算法之—— 位运算(上)
  • LeetCode 分类刷题:2187. 完成旅途的最少时间
  • Redis持久化之AOF:日志记录的艺术,数据安全保障详解
  • 应急响应-事件处理学习大纲(1)
  • 基于「YOLO目标检测 + 多模态AI分析」的遥感影像目标检测分析系统(vue+flask+数据集+模型训练)
  • 【Android】Viewpager2实现无限轮播图
  • 【前端教程】从基础到优化:一个登录页面的完善过程
  • 一文入门python中的进程、线程和协程
  • Tempus Fugit: 3靶场
  • XXL-JOB-Admin后台手动执行任务传参过长被截断问题解决
  • 【AI推理部署】Docker篇02—Docker 快速入手
  • 【C语言描述】《数据结构和算法》一 绪论与时间、空间复杂度
  • 服务器 - 从一台服务器切换至另一台服务器(损失数十条访客记录)
  • 【Android】View 交互的事件处理机制
  • 软考中级信息安全与病毒防护知识点
  • 贪心算法应用:量子密钥路径选择问题详解
  • 【算法】【链表】160.相交链表--通俗讲解
  • v-model与.aync的区别
  • 淘宝返利app的前端性能优化:从资源加载到首屏渲染的全链路优化
  • 【LeetCode】38. 外观数列
  • ZYNQ7020 Bank划分
  • 【2025】Office核心组件Microsoft word,Excel,PowerPoint详细使用指南
  • ARM编译器的__inline和 __forceinline
  • Zookeeper介绍与部署(Linux)
  • [硬件电路-216]:电场是什么?只有正电荷或只有负电荷,能产生电场吗?
  • pthread_mutex_lock函数深度解析
  • 【记录】初赛复习 Day1
  • 深入理解跳表(Skip List):原理、实现与应用
  • SciKit-Learn 全面分析 20newsgroups 新闻组文本数据集(文本分类)
  • 使用 Neo4j 和 Ollama 在本地构建知识图谱