ffplay源码分析2-数据结构

作者:leisure 本文为作者原创,转载请注明出处:https://www.cnblogs.com/leisure_chn/p/10301253.html “ffplay源码分析”系列文章如下: [1]. ffplay源码分析1-概述 [2]. ffplay源码分析2-数据结构 [3]. ffplay源码分析3-代码框架 [4]. ffplay源码分析4-音视频同步 [5]. ffplay源码分析5-图像格式转换 [6]. ffplay源码分析6-音频重采样 [7]. ffplay源码分析7-播放控制 2. 数据结构 几个关键的数据结构如下: 2.1 struct VideoState 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 typedef struct VideoState { SDL_Thread *read_tid; // demux解复用线程 AVInputFormat *iformat; int abort_request; int force_refresh; int paused; 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; int realtime; 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; double audio_clock; // 每个音频帧更新一下此值,以pts形式表示 int audio_clock_serial; // 播放序列,seek可改变此值 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音频缓冲区大小(单位字节) uint8_t *audio_buf; // 指向待播放的一帧音频数据,指向的数据区将被拷入SDL音频缓冲区。若经过重采样则指向audio_buf1,否则指向frame中的音频 uint8_t *audio_buf1; // 音频重采样的输出缓冲区 unsigned int audio_buf_size; /* in bytes */ // 待播放的一帧音频数据(audio_buf指向)的大小 unsigned int audio_buf1_size; // 申请到的音频缓冲区audio_buf1的实际尺寸 int audio_buf_index; /* in bytes */ // 当前音频帧中已拷入SDL音频缓冲区的位置索引(指向第一个待拷贝字节) int audio_write_buf_size; // 当前音频帧中尚未拷入SDL音频缓冲区的数据量,audio_buf_size = audio_buf_index + audio_write_buf_size int audio_volume; // 音量 int muted; // 静音状态 struct AudioParams audio_src; // 音频frame的参数 #if CONFIG_AVFILTER struct AudioParams audio_filter_src; #endif struct AudioParams audio_tgt; // SDL支持的音频参数,重采样转换:audio_src->audio_tgt struct SwrContext *swr_ctx; // 音频重采样context int 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; // 视频队列 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; int width, height, xleft, ytop; 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 int last_video_stream, last_audio_stream, last_subtitle_stream; SDL_cond *continue_read_thread; } VideoState; 2.2 struct Clock 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 typedef struct Clock { // 当前帧(待播放)显示时间戳,播放后,当前帧变成上一帧 double pts; /* clock base */ // 当前帧显示时间戳与当前系统时钟时间的差值 double pts_drift; /* clock base minus time at which we updated the clock */ // 当前时钟(如视频时钟)最后一次更新时间,也可称当前时钟时间 double last_updated; // 时钟速度控制,用于控制播放速度 double speed; // 播放序列,所谓播放序列就是一段连续的播放动作,一个seek操作会启动一段新的播放序列 int serial; /* clock is based on a packet with this serial */ // 暂停标志 int paused; // 指向packet_serial int *queue_serial; /* pointer to the current packet queue serial, used for obsolete clock detection */ } Clock; 2.3 struct PacketQueue 1 2 3 4 5 6 7 8 9 10 typedef struct PacketQueue { MyAVPacketList *first_pkt, *last_pkt; int nb_packets; // 队列中packet的数量 int size; // 队列所占内存空间大小 int64_t duration; // 队列中所有packet总的播放时长 int abort_request; int serial; // 播放序列,所谓播放序列就是一段连续的播放动作,一个seek操作会启动一段新的播放序列 SDL_mutex *mutex; SDL_cond *cond; } PacketQueue; 栈(LIFO)是一种表,队列(FIFO)也是一种表。数组是表的一种实现方式,链表也是表的一种实现方式,例如FIFO既可以用数组实现,也可以用链表实现。PacketQueue是用链表实现的一个FIFO。 2.4 struct FrameQueue 1 2 3 4 5 6 7 8 9 10 11 12 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; // 指向对应的packet_queue } FrameQueue; FrameQueue是一个环形缓冲区(ring buffer),是用数组实现的一个FIFO。下面先讲一下环形缓冲区的基本原理,其示意图如下: ring buffer示意图 环形缓冲区的一个元素被用掉后,其余元素不需要移动其存储位置。相反,一个非环形缓冲区在用掉一个元素后,其余元素需要向前搬移。换句话说,环形缓冲区适合实现FIFO,而非环形缓冲区适合实现LIFO。环形缓冲区适合于事先明确了缓冲区的最大容量的情形。扩展一个环形缓冲区的容量,需要搬移其中的数据。因此一个缓冲区如果需要经常调整其容量,用链表实现更为合适。 环形缓冲区使用中要避免读空和写满,但空和满状态下读指针和写指针均相等,因此其实现中的关键点就是如何区分出空和满。有多种策略可以用来区分空和满的标志: 1) 总是保持一个存储单元为空:“读指针”==“写指针”时为空,“读指针”==“写指针+1”时为满; 2) 使用有效数据计数:每次读写都更新数据计数,计数等于0时为空,等于BUF_SIZE时为满; 3) 记录最后一次操作:用一个标志记录最后一次是读还是写,在“读指针”==“写指针”时若最后一次是写,则为满状态;若最后一次是读,则为空状态。 可以看到,FrameQueue使用上述第2种方式,使用FrameQueue.size记录环形缓冲区中元素数量,作为有效数据计数。 ffplay中创建了三个frame_queue:音频frame_queue,视频frame_queue,字幕frame_queue。每一个frame_queue一个写端一个读端,写端位于解码线程,读端位于播放线程。 为了叙述方便,环形缓冲区的一个元素也称作节点(或帧),将rindex称作读指针或读索引,将windex称作写指针或写索引,叫法用混用的情况,不作文字上的严格区分。 2.4.1 初始化与销毁 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 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,细节不展开。 f->keep_last是队列中是否保留最后一次播放的帧的标志。f->keep_last = !!keep_last是将int取值的keep_last转换为boot取值(0或1)。 1 2 3 4 5 6 7 8 9 10 11 static void frame_queue_destory(FrameQueue *f) { int i; for (i = 0; i < f->max_size; i++) { Frame *vp = &f->queue[i]; frame_queue_unref_item(vp); // 释放对vp->frame中的数据缓冲区的引用,注意不是释放frame对象本身 av_frame_free(&vp->frame); // 释放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对象本身 2.4.2 写队列 写队列的步骤是: 1) 获取写指针(若写满则等待); 2) 将元素写入队列; 3) 更新写指针。 写队列涉及下列两个函数: 1 2 frame_queue_peek_writable() // 获取写指针 frame_queue_push() // 更新写指针 通过实例看一下写队列的用法: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 static int queue_picture(VideoState *is, AVFrame *src_frame, double pts, double duration, int64_t pos, int serial) { Frame *vp; 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); av_frame_move_ref(vp->frame, src_frame); frame_queue_push(&is->pictq); return 0; } 上面一段代码是视频解码线程向视频frame_queue中写入一帧的代码,步骤如下: 1) frame_queue_peek_writable(&is->pictq)向队列尾部申请一个可写的帧空间,若队列已满无空间可写,则等待 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写操作相关函数实现如下: frame_queue_peek_writable() 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 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]; } 向队列尾部申请一个可写的帧空间,若无空间可写,则等待 frame_queue_push() 1 2 3 4 5 6 7 8 9 static void frame_queue_push(FrameQueue *f) { if (++f->windex == f->max_size) f->windex = 0; SDL_LockMutex(f->mutex); f->size++; SDL_CondSignal(f->cond); SDL_UnlockMutex(f->mutex); } 向队列尾部压入一帧,只更新计数与写指针,因此调用此函数前应将帧数据写入队列相应位置 2.4.3 读队列 写队列中,应用程序写入一个新帧后通常总是将写指针加1。而读队列中,“读取”和“更新读指针(同时删除旧帧)”二者是独立的,可以只读取而不更新读指针,也可以只更新读指针(只删除)而不读取。而且读队列引入了是否保留已显示的最后一帧的机制,导致读队列比写队列要复杂很多。 读队列和写队列步骤是类似的,基本步骤如下: 1) 获取读指针(若读空则等待); 2) 读取一个节点; 3) 更新写指针(同时删除旧节点)。 写队列涉及如下函数: 1 2 3 4 5 frame_queue_peek_readable() // 获取读指针(若读空则等待) frame_queue_peek() // 获取当前节点指针 frame_queue_peek_next() // 获取下一节点指针 frame_queue_peek_last() // 获取上一节点指针 frame_queue_next() // 更新读指针(同时删除旧节点) 通过实例看一下读队列的用法: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 static void video_refresh(void *opaque, double *remaining_time) { ...... if (frame_queue_nb_remaining(&is->pictq) == 0) { // 所有帧已显示 // nothing to do, no picture to display in the queue } else { Frame *vp, *lastvp; lastvp = frame_queue_peek_last(&is->pictq); // 上一帧:上次已显示的帧 vp = frame_queue_peek(&is->pictq); // 当前帧:当前待显示的帧 frame_queue_next(&is->pictq); // 删除上一帧,并更新rindex video_display(is)-->video_image_display()-->frame_queue_peek_last(); } ...... } 上面一段代码是视频播放线程从视频frame_queue中读取视频帧进行显示的基本步骤,其他代码已省略,只保留了读队列部分。video_refresh()的实现详情可参考第3节。 记lastvp为上一次已播放的帧,vp为本次待播放的帧,下图中方框中的数字表示显示序列中帧的序号(实际就是Frame.frame.display_picture_number变量值)。 frame_queue示意图 在启用keep_last机制后,rindex_shown值总是为1,rindex_shown确保了最后播放的一帧总保留在队列中。 假设某次进入video_refresh()的时刻为T0,下次进入的时刻为T1。在T0时刻,读队列的步骤如下: 1) rindex(图中ri)表示上一次播放的帧lastvp,本次调用video_refresh()中,lastvp会被删除,rindex会加1 2) rindex+rindex_shown(图中ris)表示本次待播放的帧vp,本次调用video_refresh()中,vp会被读出播放 图中已播放的帧是灰色方框,本次待播放的帧是黑色方框,其他未播放的帧是绿色方框,队列中空位置为白色方框。 在之后的某一时刻TX,首先调用frame_queue_nb_remaining()判断是否有帧未播放,若无待播放帧,函数video_refresh()直接返回,不往下执行。 1 2 3 4 5 /* 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为2,队列中就会保留最后2帧。 但keep_last机制有什么用途呢?希望知道的同学指点一下。 注意,在TX时刻,无新帧可显示,保留的一帧是已经显示过的。那么最后一帧什么时候被清掉呢?在播放结束或用户中途取消播放时,会调用frame_queue_destory()清空播放队列。 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。如果rindex_shown对理解代码造成了困扰,可以先将全局变量keep_last值赋为0,这样f->rindex_shown值为0,代码看起来会清晰很多。理解了读队列的基本方法后,再看f->rindex_shown值为1时代码是如何运行的。 先看frame_queue_next()函数: frame_queue_next() 1 2 3 4 5 6 7 8 9 10 11 12 13 14 static vo
50000+
5万行代码练就真实本领
17年
创办于2008年老牌培训机构
1000+
合作企业
98%
就业率

联系我们

电话咨询

0532-85025005

扫码添加微信