ffplay源码分析2-数据结构 (3)

队列销毁函数对队列中的每个节点作了如下处理:
1) frame_queue_unref_item(vp)释放本队列对vp->frame中AVBuffer的引用
2) av_frame_free(&vp->frame)释放vp->frame对象本身

2.4.2 写队列

写队列的步骤是:
1) 获取写指针(若写满则等待);
2) 将元素写入队列;
3) 更新写指针。
写队列涉及下列两个函数:

frame_queue_peek_writable() // 获取写指针 frame_queue_push() // 更新写指针

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

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()

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()

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) 更新写指针(同时删除旧节点)。
写队列涉及如下函数:

frame_queue_peek_readable() // 获取读指针(若读空则等待) frame_queue_peek() // 获取当前节点指针 frame_queue_peek_next() // 获取下一节点指针 frame_queue_peek_last() // 获取上一节点指针 frame_queue_next() // 更新读指针(同时删除旧节点)

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

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变量值)。

在启用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()直接返回,不往下执行。

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

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/wpxxfg.html