本篇文章看看ffplay是如何读取packet放入队列中的。

一、先看入口函数:

int main(int argc, char **argv)
{VideoState *is;...av_init_packet(&flush_pkt);flush_pkt.data = (uint8_t *)&flush_pkt;...is = stream_open(input_filename, file_iformat);if (!is) {av_log(NULL, AV_LOG_FATAL, "Failed to initialize VideoState!\n");do_exit(NULL);}event_loop(is);/* never returns */return 0;
}

main函数中,我做了一下简化。首先对全局变量flush_pkt进行初始化,让其data数据指向其自身。flush_pkt的作用是便于后面要说的seek操作的。然后调用stream_open()打开流,event_loop()后面再说。

二、下面是stream_open函数:

static VideoState *stream_open(const char *filename, AVInputFormat *iformat)
{VideoState *is;is = av_mallocz(sizeof(VideoState));if (!is)return NULL;/* start video display */if (frame_queue_init(&is->pictq, &is->videoq, VIDEO_PICTURE_QUEUE_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;if (packet_queue_init(&is->videoq) < 0 ||packet_queue_init(&is->audioq) < 0 ||packet_queue_init(&is->subtitleq) < 0)goto fail;init_clock(&is->vidclk, &is->videoq.serial);init_clock(&is->audclk, &is->audioq.serial);init_clock(&is->extclk, &is->extclk.serial);is->read_tid     = SDL_CreateThread(read_thread, "read_thread", is);if (!is->read_tid) {av_log(NULL, AV_LOG_FATAL, "SDL_CreateThread(): %s\n", SDL_GetError());fail:stream_close(is);return NULL;}return is;
}

stream_open同样也做了一些简化,av_mallocz为VideoState创建了内存空间,并把初始值设置为0。然后调用frame_queue_init函数初始化三个FrameQueue队列,接着调用packet_queue_init初始化三个PacketQueue队列,然后调用init_clock函数对三个时钟做了简单的初始化工作。最后调用SDL_CreateThread(read_thread, "read_thread", is)创建了一个read_thread线程,用于读取文件获取packet。

三、下面先看看frame_queue_init函数和packet_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()))return AVERROR(ENOMEM);return 0;
}

可以看到,frame_queue_init函数中创建了锁(SDL_CreateMutex)和条件变量(SDL_CreateCond),下面会讲到其用处。f->pktq = pktq;设置了帧队列所对应的packet队列(也就是视频帧队列对应视频packet队列等等),f->max_size = FFMIN(max_size, FRAME_QUEUE_SIZE)设置了帧队列的最大容量,!!keep_last将keep_last转换为布尔值(音频帧队列和视频帧队列,初始值为1)。for循环,创建了帧队列中的每个节点的frame。

packet_queue_init函数和frame_queue_init函数差不多,仅创建了packet队列的锁和条件变量:

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

四、最后便是read_thread函数,此函数比较长,做一些简化:

static int read_thread(void *arg)
{.../*设置中断回调,当读取网络流时,如果遇到网络不好,等待时间较长时,会回调此回调函数,以询问开发者是否要中断读取网络流。*/ic->interrupt_callback.callback = decode_interrupt_cb;ic->interrupt_callback.opaque = is;.../*下面这三个stream_component_open函数分别打开音频流,视频流,字幕流,并在其中开启了解码线程,留待后面再说*/if (st_index[AVMEDIA_TYPE_AUDIO] >= 0) {stream_component_open(is, st_index[AVMEDIA_TYPE_AUDIO]);}ret = -1;if (st_index[AVMEDIA_TYPE_VIDEO] >= 0) {ret = stream_component_open(is, st_index[AVMEDIA_TYPE_VIDEO]);}if (st_index[AVMEDIA_TYPE_SUBTITLE] >= 0) {stream_component_open(is, st_index[AVMEDIA_TYPE_SUBTITLE]);}...//这个for循环,是循环读取文件,获取到packet后,放入到队列中for (;;) {//如果丢弃标志为1,则结束循环,结束线程if (is->abort_request)break;//网络流的开始和暂停if (is->paused != is->last_paused) {is->last_paused = is->paused;if (is->paused)is->read_pause_return = av_read_pause(ic);elseav_read_play(ic);}
#if CONFIG_RTSP_DEMUXER || CONFIG_MMSH_PROTOCOL//如果暂停并且输入流不是rtsp和mmsh,则延时10ms,再继续if (is->paused &&(!strcmp(ic->iformat->name, "rtsp") ||(ic->pb && !strncmp(input_filename, "mmsh:", 5)))) {/* wait 10 ms to avoid trying to get another packet *//* XXX: horrible */SDL_Delay(10);continue;}
#endif/*当进行seek操作时(按下键盘的左右键),会将seek_req置为1*/if (is->seek_req) {//seek的目标位置int64_t seek_target = is->seek_pos;int64_t seek_min    = is->seek_rel > 0 ? seek_target - is->seek_rel + 2: INT64_MIN;int64_t seek_max    = is->seek_rel < 0 ? seek_target - is->seek_rel - 2: INT64_MAX;// FIXME the +-2 is due to rounding being not done in the correct direction in generation//      of the seek_pos/seek_rel variables//seek到目标位置ret = avformat_seek_file(is->ic, -1, seek_min, seek_target, seek_max, is->seek_flags);if (ret < 0) {av_log(NULL, AV_LOG_ERROR,"%s: error while seeking\n", is->ic->url);} else {    //seek成功if (is->audio_stream >= 0) {/*删掉队列中的所有packet,因为seek操作后,队列中之前存在的packet不应该被解码播放了*/packet_queue_flush(&is->audioq);//放入一个flush_pkt。packet_queue_put(&is->audioq, &flush_pkt);}if (is->subtitle_stream >= 0) {packet_queue_flush(&is->subtitleq);packet_queue_put(&is->subtitleq, &flush_pkt);}if (is->video_stream >= 0) {packet_queue_flush(&is->videoq);packet_queue_put(&is->videoq, &flush_pkt);}if (is->seek_flags & AVSEEK_FLAG_BYTE) {set_clock(&is->extclk, NAN, 0);} else {set_clock(&is->extclk, seek_target / (double)AV_TIME_BASE, 0);}}//将seek_req重新置为0is->seek_req = 0;is->queue_attachments_req = 1;is->eof = 0;if (is->paused)step_to_next_frame(is);}if (is->queue_attachments_req) {if (is->video_st && is->video_st->disposition & AV_DISPOSITION_ATTACHED_PIC) {AVPacket copy = { 0 };if ((ret = av_packet_ref(&copy, &is->video_st->attached_pic)) < 0)goto fail;packet_queue_put(&is->videoq, &copy);packet_queue_put_nullpacket(&is->videoq, is->video_stream);}is->queue_attachments_req = 0;}/* 如果packet队列满了,则等待10ms后,再继续 */if (infinite_buffer<1 &&(is->audioq.size + is->videoq.size + is->subtitleq.size > MAX_QUEUE_SIZE|| (stream_has_enough_packets(is->audio_st, is->audio_stream, &is->audioq) &&stream_has_enough_packets(is->video_st, is->video_stream, &is->videoq) &&stream_has_enough_packets(is->subtitle_st, is->subtitle_stream, &is->subtitleq)))) {/* wait 10 ms */SDL_LockMutex(wait_mutex);SDL_CondWaitTimeout(is->continue_read_thread, wait_mutex, 10);SDL_UnlockMutex(wait_mutex);continue;}//没有暂停 && (有音频流 || (音频解码结束 && 音频帧队列没有帧了)) && ...if (!is->paused &&(!is->audio_st || (is->auddec.finished == is->audioq.serial && frame_queue_nb_remaining(&is->sampq) == 0)) &&(!is->video_st || (is->viddec.finished == is->videoq.serial && frame_queue_nb_remaining(&is->pictq) == 0))) {//loop表示循环,是否要循环播放,循环播放几次if (loop != 1 && (!loop || --loop)) {stream_seek(is, start_time != AV_NOPTS_VALUE ? start_time : 0, 0, 0);} else if (autoexit) {   //是否要自动退出ret = AVERROR_EOF;goto fail;}}//读packetret = av_read_frame(ic, pkt);if (ret < 0) {if ((ret == AVERROR_EOF || avio_feof(ic->pb)) && !is->eof) {if (is->video_stream >= 0)//读到文件尾,放入空packet,用以flush解码器,获得解码器内缓存的几帧packet_queue_put_nullpacket(&is->videoq, is->video_stream);if (is->audio_stream >= 0)packet_queue_put_nullpacket(&is->audioq, is->audio_stream);if (is->subtitle_stream >= 0)packet_queue_put_nullpacket(&is->subtitleq, is->subtitle_stream);is->eof = 1;}if (ic->pb && ic->pb->error)break;SDL_LockMutex(wait_mutex);SDL_CondWaitTimeout(is->continue_read_thread, wait_mutex, 10);SDL_UnlockMutex(wait_mutex);continue;} else {is->eof = 0;}/* check if packet is in play range specified by user, then queue, otherwise discard */stream_start_time = ic->streams[pkt->stream_index]->start_time;pkt_ts = pkt->pts == AV_NOPTS_VALUE ? pkt->dts : pkt->pts;/*pkt_in_play_range计算得到的packet是否在播放范围内由于ffplay可以设定只播放前10秒的内容(duration),那么当然要判断pkt是否在前10秒之内,如果未设定duration,即duration == AV_NOPTS_VALUE,则pkt_in_play_range为真*/pkt_in_play_range = duration == AV_NOPTS_VALUE ||(pkt_ts - (stream_start_time != AV_NOPTS_VALUE ? stream_start_time : 0)) *av_q2d(ic->streams[pkt->stream_index]->time_base) -(double)(start_time != AV_NOPTS_VALUE ? start_time : 0) / 1000000<= ((double)duration / 1000000);if (pkt->stream_index == is->audio_stream && pkt_in_play_range) {//如果是音频packet并且在播放范围内,则放入音频队列packet_queue_put(&is->audioq, pkt);} else if (pkt->stream_index == is->video_stream && pkt_in_play_range&& !(is->video_st->disposition & AV_DISPOSITION_ATTACHED_PIC)) {packet_queue_put(&is->videoq, pkt);} else if (pkt->stream_index == is->subtitle_stream && pkt_in_play_range) {packet_queue_put(&is->subtitleq, pkt);} else {av_packet_unref(pkt);}}ret = 0;
fail:if (ic && !is->ic)avformat_close_input(&ic);if (ret != 0) {SDL_Event event;event.type = FF_QUIT_EVENT;event.user.data1 = is;SDL_PushEvent(&event);}SDL_DestroyMutex(wait_mutex);return 0;
}

在read_thread函数中写了一些注释了,下面挑一些重点说一下。

1、清空队列

如果进行seek操作,成功后,则首先要清除掉packet队列中所有的packet,因为这些packet是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;//释放节点所引用的packet->dataav_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);
}

(1)可以看到,packet_queue_flush函数内部循环链表,删掉每个节点,并进行一些释放内存空间的操作。

(2)清空时加了锁。对于packet队列,由于是多线程访问(read_thread线程写入,解码线程读取),必须加锁,SDL库提供了相应的功能,对于帧队列来说,也同样得加锁(解码线程写入,主线程读取)。

2.放入flush_pkt

每一次seek,清空队列后,需要放入flush_pkt。flush_pkt的作用是,当解码线程从packet队列中获取packet时,如果获取到的是flush_pkt,则清除解码器内部的状态以及内部缓存的帧,这样才能解码seek操作后的新帧。

3.packet_queue_put

packet_queue_put函数可以放入flush_pkt,也可以放入正常的音视频packet,下面看一下相关代码:

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;
}
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;pkt1->next = NULL;if (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;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仅是把packet_queue_put_private函数加了锁,重点是packet_queue_put_private函数。

packet_queue_put_private函数作用是,构造链表节点,放入队列。其中可以看到:

    if (pkt == &flush_pkt)q->serial++;pkt1->serial = q->serial;

这三行代码的作用是,每次放入packet到队列时,如果packet是flush_pkt,则队列的serial自增1,以后放入队列中的packet的serial也和队列的serial相等。这样的目的是,当seek操作后,从packet队列中取出的packet的serial如果和队列的serial值不一样,则表明此packet是seek前的,应该继续从队列取packet,直到packet.serial == packetQueue.serial。

4.queue_attachments_req

queue_attachments_req表示带attachments的流,比如带封面图片的mp3,有音频流,也有视频流,但是视频流只有一个帧,就是封面图片,存于流的attached_pic字段中,所以ffplay做了特殊的判断。

下一篇文章解读一下解码线程。

35.FFmpeg学习笔记 - ffplay源码解读3之读文件相关推荐

  1. 34.FFmpeg学习笔记 - ffplay源码解读2之数据结构

    本篇分析一下ffplay的数据结构. (1)VideoState VideoState结构体,正如名字的含义,管理了一些全局的播放状态. typedef struct VideoState {SDL_ ...

  2. yolov1-v5学习笔记及源码解读

    目录 深度学习网络分类 评价指标 原理 yolov1 yolov2 yolov3 yolov4 yolov5 源码解读(v3为例) 深度学习网络分类 深度学习经典检测方法 通常分为 two-stage ...

  3. Opencv学习笔记 - imread源码解读

    一.打开图片流程分析 1.读取图片头,进行解码器的寻找 2.根据参数flags,确定图像通道和是否缩放 3.给解码器指定缩放参数和源 4.使用解码器读取图像的头,确保没有问题,失败则输出错误并返回 5 ...

  4. sheng的学习笔记-Vector源码分析

    概述 Vector底层也是数组,跟ArrayList很像(先看下ArrayList,再看Vector会很轻松),ArrayList可参考下文,并且由于效率低,已经被淘汰了,大概瞅瞅得了 sheng的学 ...

  5. jpcsp源码解读6:PSF文件

    当你运行了模拟器,通过模拟器菜单选择并加载一个umd镜像,模拟器就用这个umd镜像实例化一个UmdIsoReader(见上一篇,源码解读5). 通过这个UmdIsoReader,从光盘提取的第一个文件 ...

  6. PixHawk学习笔记 之 源码浅析——mc_pos_control.cpp——task_main

    注意:基于"Firmware-1.6.0rc1" 献上固件源码分享链接:https://pan.baidu.com/s/1kUPocmF 密码:j55a 自己边学边写的,一定有错, ...

  7. 狂神说SpringCloud学习笔记(附带源码和笔记)

    狂神说Spring Cloud Netflix笔记-01(服务注册与发现) 狂神说Spring Cloud Netflix笔记-02(Eureka集群的搭建 ) 狂神说Spring Cloud Net ...

  8. android源码编译 简书,android学习笔记之源码编译

    编译环境 1.需要Ubuntu 64bit,建议Ubuntu14.04 64-bit 2.安装openJDK7 $ sudo apt-get update $ sudo apt-get install ...

  9. dubbo学习笔记 一 源码编译

    前面学习了netty和rocketmq,当然前面的文章还会继续更新,继续往下写 2016 没几天了,我打算写下dubbo 2017 继续深入源码,大家有啥问题 都可以一起来讨论 源码搭建 下载源码 同 ...

最新文章

  1. 实时分布式搜索引擎比较(senseidb、Solr、elasticsearch)
  2. 计算机开启时提示键盘错误,电脑开机出现异常提示keyboard not found的故障原因及解决方法_电脑故障...
  3. linux lddbus设备,Linux那些事儿之我是Sysfs(4)举例一lddbus | 技术部落
  4. 有关计算机代码的游戏,七灯游戏是一款经典的益智类游戏。游戏中,有七盏灯排成一圈,如图a所示,初始时灯的开关状态随机生成,操作其中某一盏灯,则可以切换该灯的“开/关”状态,同时,这盏灯-组卷网...
  5. 圈钱跑路 ERC20 Token 合约代码分析
  6. linux下单独安装oracle12.1客户端
  7. es6解决回调地狱问题
  8. Python 字符串和列表的转化 ,简单到尖叫
  9. 百旺最新服务器地址,百旺金赋安装与使用教程
  10. Linux安装 conda 时报错:WARNING: md5sum mismatch
  11. 「驱动安装」HighPoint RocketRAID R2722 磁盘阵列卡 驱动安装教程
  12. 重启服务器后docker wordpress “Error establishing a database connection”解决办法
  13. 聚焦交通缓堵之东城篇,核心区如何重拳治堵
  14. 篮桥杯,翻硬币 (贪心)
  15. STRATASYS 即将收购 ORIGIN,将全新增材制造平台引入聚合物生产领域
  16. 关于买房提前还款问题
  17. vs2010+opencv2.4.9配置========重点说明
  18. python樱花树代码_Python画樱花树
  19. SDU信息门户(8)组队和文件系统分析
  20. nmap常用命令/使用教程

热门文章

  1. 高知的程序员必须甩脱穷人思维
  2. 最近几年我买的一些技术书的随书光盘CD
  3. Java笔记(10)
  4. 软件测试面试中的一些奇葩问题
  5. html 百度网盘的布局,百度网盘披露5G布局:打造个人云操作系统!
  6. TS学习(二) :安装ts与ts配置
  7. matlab练习(11.7)
  8. 一致性哈希算法的解析与实现
  9. 初中计算机函数的使用教案,初中《函数的使用》说课稿
  10. mysql数据库进阶书_mysql数据库进阶篇