【音视频】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 computationdouble audio_diff_avg_coef;double audio_diff_threshold;int audio_diff_avg_count;// endAVStream *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_sizeint 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; // 视频队列double max_frame_duration; // ⼀帧最⼤间隔. above this, we consider the jump a timestamp discontinuitystruct 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;SDL_cond *continue_read_thread; // 当读取数据队列满了后进⼊休眠时,可以通过该condition唤醒读线程
} VideoState;
struct Clock 时钟封装
typedef struct Clock {double pts; // 时钟基础, 当前帧(待播放)显示时间戳,播放后,当前帧变成上⼀帧// 当前pts与当前系统时钟的差值, audio、video对于该值是独⽴的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 serialint 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; // 播放序列号,和MyAVPacketList的serial作⽤相同SDL_mutex *mutex; // ⽤于维持PacketQueue的多线程安全(SDL_mutex可以按pthread_mutex_t理解)SDL_cond *cond; // ⽤于读、写线程相互通知(SDL_cond可以按pthread_cond_t理解)
} 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:
/* 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; // 在packet_queue_start和packet_queue_abort时修改到该值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()
- packet_queue_start()
static void packet_queue_start(PacketQueue *q)
{SDL_LockMutex(q->mutex);packet_queue_destroy()packet_queue_start()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()
- 中⽌队列:
- 这⾥SDL_CondSignal的作⽤在于确保当前等待该条件的线程能被激活并继续执⾏退出流程,并唤醒者会检测abort_request标志确定⾃⼰的退出流程。
static void packet_queue_abort(PacketQueue *q)
{SDL_LockMutex(q->mutex);q->abort_request = 1; // 请求退出SDL_CondSignal(q->cond); //释放⼀个条件信号SDL_UnlockMutex(q->mutex);
}
packet_queue_put()
- 读、写是PacketQueue的主要⽅法。
- 先看写——往队列中放⼊⼀个节点:
- 主要实现在函数 packet_queue_put_private ,这⾥需要注意的是如果插⼊失败,则需要释放AVPacket。
static int packet_queue_put(PacketQueue *q, AVPacket *pkt)
{int ret;packet_queue_abort()packet_queue_put()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
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。pkt1->pkt = *pkt; //拷⻉AVPacket(浅拷⻉,AVPacket.data等内存并没有拷⻉)pkt1->next = NULL;if (pkt == &flush_pkt) //如果放⼊的是flush_pkt,需要增加队列的播放序列{q->serial++;printf("q->serial = %d\n", q->serial++);}pkt1->serial = q->serial; //⽤队列序列号标记节点/* 队列操作:如果last_pkt为空,说明队列是空的,新增节点为队头;* 否则,队列有数据,则让原队尾的next为新增节点。 最后将队尾指向新增节点*/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主要完成3件事:
- 计算serial。serial标记了这个节点内的数据是何时的。⼀般情况下新增节点与上⼀个节点的serial是⼀
样的,但当队列中加⼊⼀个flush_pkt后,后续节点的serial会⽐之前⼤1,⽤来区别不同播放序列的packet. - 节点⼊队列操作。
- 队列属性操作。更新队列中节点的数⽬、占⽤字节数(含AVPacket.data的⼤⼩)及其时⻓。主要⽤来控制Packet队列的⼤⼩,我们PacketQueue链表式的队列,在内存充⾜的条件下我们可以⽆限put⼊packet,如果我们要控制队列⼤⼩,则需要通过其变量size、duration、nb_packets三者单⼀或者综合去约束队列的节点的数量,具体在read_thread进⾏分析。
packet_queue_get()
- 从队列中取一个节点
该函数整体流程:
- 加锁
- 进⼊for循环,如果需要退出for循环,则break;当没有数据可读且block为1时则等待
- ret = -1 终⽌获取packet
- ret = 0 没有读取到packet
- ret = 1 获取到了packet
- 释放锁
如果有取到数据,主要分3个步骤:
- 队列操作:出队列操作; nb_packets相应-1; duration 的也相应减少, size也相应占⽤的字节
⼤⼩(pkt1->pkt.size + sizeof(*pkt1)) - 给输出参数赋值:就是MyAVPacketList的成员传递给输出参数pkt和serial
- 释放节点内存:释放放⼊队列时申请的节点内存(注意是节点内存⽽不是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); // 释放AVPacket的数据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的实现和主要的操作⽅法,现在总结下两个关键的点:
第⼀,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设计思路:
- 设计⼀个多线程安全的队列,保存AVPacket,同时统计队列内已缓存的数据⼤⼩。(这个统计数据会⽤来后续设置要缓存的数据量)
- 引⼊serial的概念,区别前后数据包是否连续,主要应⽤于seek操作。
- 设计了两类特殊的packet——flush_pkt和nullpkt(类似⽤于多线程编程的事件模型——往队列中放⼊flush事件、放⼊null事件),我们在⾳频输出、视频输出、播放控制等模块时也会继续对flush_pkt和nullpkt的作⽤展开分析。
struct Frame 和 FrameQueue队列
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 AVSampleFormat)AVRational 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变量)。
FrameQueue
typedef struct FrameQueue {Frame queue[FRAME_QUEUE_SIZE]; // FRAME_QUEUE_SIZE 最⼤size, 数字太⼤时会占⽤⼤量的内存,需要注意该值的设置int rindex; // 读索引。待播放时读取此帧进⾏播放,播放后此帧成为上⼀帧struct Frame 和 FrameQueue队列int windex; // 写索引int size; // 当前总帧数int max_size; // 可存储最⼤帧数int keep_last; // = 1说明要在队列⾥⾯保持最后⼀帧的数据不释放,只在销毁队列的时候才将其真正释放int rindex_shown; // 初始化为0,配合keep_last=1使⽤SDL_mutex *mutex; // 互斥量0 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 ma
x_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(
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);
}
队列销毁函数对队列中的每个节点作了如下处理:
- frame_queue_unref_item(vp) 释放本队列对vp->frame中AVBuffer的引⽤
- av_frame_free(&vp->frame) 释放vp->frame对象本身
frame_queue_peek_writable()获取可写Frame、frame_queue_push()⼊队列
FrameQueue写队列的步骤和PacketQueue不同,分了3步进⾏:
- 调⽤frame_queue_peek_writable获取可写的Frame,如果队列已满则等待
- 获取到Frame后,设置Frame的成员变量
- 再调⽤frame_queue_push更新队列的写索引,真正将Frame⼊队列
Frame *frame_queue_peek_writable(FrameQueue *f); // 获取可写帧
void frame_queue_push(FrameQueue *f);
通过实例看⼀下写队列的⽤法:
// video AVFrame ⼊队列
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; // Frame队列满了则返回-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);av_frame_move_ref(vp->frame, src_frame); // 将src中所有数据拷⻉到dst中,并复位src。frame_queue_push(&is->pictq); // 更新写索引位置return 0;
}
上⾯⼀段代码是视频解码线程向视频frame_queue中写⼊⼀帧的代码,步骤如下:
-
frame_queue_peek_writable(&is->pictq)
向队列尾部申请⼀个可写的帧空间,若队列已满⽆空间可写,则等待(由SDL_cond *cond控制,由frame_queue_next或frame_queue_signal触发唤醒) -
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。 -
frame_queue_push(&is->pictq)
此步仅将frame_queue中的写索引加1,实际的数据写⼊在此步之前已经完成。
frame_queue_peek_writable获取可写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];
}
frame_queue_push()
- 向队列尾部压⼊⼀帧,只更新计数与写指针,因此调⽤此函数前应将帧数据写⼊队列相应位置
SDL_CondSignal(f->cond)
;可以唤醒读frame_queue_peek_readable。
// 更新写索引
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); // 当frame_queue_peek_readable在等待时则可以唤醒SDL_UnlockMutex(f->mutex);
}
frame_queue_peek_readable() 获取可读Frame、frame_queue_next()出队列
- 写队列中,应⽤程序写⼊⼀个新帧后通常总是将写索引加1。⽽读队列中,“读取”和“更新读索引(同时删除旧帧)”⼆者是独⽴的,可以只读取⽽不更新读索引,也可以只更新读索引(只删除)⽽不读取(只有更新读索引的时候才真正释放对应的Frame数据)。⽽且读队列引⼊了是否保留已显示的最后⼀帧的机制,导致读队列⽐写队列要复杂很多。
读队列和写队列步骤是类似的,基本步骤如下:
- 调⽤frame_queue_peek_readable获取可读Frame;
- 如果需要更新读索引(出队列该节点)则调⽤frame_queue_peek_next;
读队列涉及如下函数:
Frame *frame_queue_peek_readable(FrameQueue *f); // 获取可读Frame指针(若读空则等待)
Frame *frame_queue_peek(FrameQueue *f); // 获取当前Frame指针
Frame *frame_queue_peek_next(FrameQueue *f); // 获取下⼀Frame指针
Frame *frame_queue_peek_last(FrameQueue *f); // 获取上⼀Frame指针
void frame_queue_next(FrameQueue *f); // 更新读索引(同时删除旧frame)
通过实例看⼀下读队列的⽤法:
- 下⾯⼀段代码是视频播放线程从视频frame_queue中读取视频帧进⾏显示的基本步骤,其他
代码已省略,只保留了读队列部分。
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;frame_queue_peek_readable() //获取可读Frameframe_queue_next()出队列lastvp = frame_queue_peek_last(&is->pictq); // 上⼀帧:上次已显示的帧vp = frame_queue_peek(&is->pictq); // 当前帧:当前待显示的帧frame_queue_next(&is->pictq); // 出队列,并更新rindexvideo_display(is)-->video_image_display()-->frame_queue_peek_last();}
......}
- 记lastvp为上⼀次已播放的帧,vp为本次待播放的帧,下图中⽅框中的数字表示显示序列中帧的序号:
- 在启⽤keep_last机制后,rindex_shown值总是为1,rindex_shown确保了最后播放的⼀帧总保留在队列中。
假设某次进⼊ video_refresh() 的时刻为T0,下次进⼊的时刻为T1。在T0时刻,读队列的步骤如下:
-
rindex表示上⼀次播放的帧lastvp,本次调⽤ video_refresh() 中,lastvp会被删除,rindex会加1,即是当调⽤frame_queue_next删除的是lastvp,⽽不是当前的vp,当前的vp转为lastvp。
-
rindex+rindex_shown表示本次待播放的帧vp,本次调⽤ video_refresh() 中,vp会被读出播放图中已播放的帧是灰⾊⽅框,本次待播放的帧是红⾊⽅框,其他未播放的帧是绿⾊⽅框,队列中空位置为⽩⾊⽅框。
-
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。
- index_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; // 第⼀次调⽤置为1return;}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);
}
主要步骤:
- 在启⽤keeplast时,如果rindex_shown为0则将其设置为1,并返回。此时并不会更新读索引。也就是说keeplast机制实质上也会占⽤着队列Frame的size,当调⽤frame_queue_nb_remaining()获取size时并不能将其计算⼊size;
- 释放Frame对应的数据(⽐如AVFrame的数据),但不释放Frame本身
- 更新读索引
- 释放唤醒信号,以唤醒正在等待写⼊的线程。
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; // 通道数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
相关文章:
【音视频】ffplay数据结构分析
struct VideoState 播放器封装 typedef struct VideoState {SDL_Thread *read_tid; // 读线程句柄AVInputFormat *iformat; // 指向demuxerint abort_request; // 1时请求退出播放int force_refresh; // 1时需要刷新画⾯,请求⽴即刷新画⾯的意思int paused; // 1时…...
PV操作:宣帧闯江湖武林客栈版学习笔记【操作系统】
P,V,S江湖话翻译 P(申请) 江湖侠客拔剑大喊“掌柜的,给我一间上房!”(申请资源,房不够就蹲门口等)-要房令牌 V(释放) 江湖侠客退房时甩出一锭银子,大喊“…...
精品推荐-湖仓一体电商数据分析平台实践教程合集(视频教程+设计文档+完整项目代码)
精品推荐,湖仓一体电商数据分析平台实践教程合集,包含视频教程、设计文档及完整项目代码等资料,供大家学习。 1、项目背景介绍及项目架构 2、项目使用技术版本及组件搭建 3、项目数据种类与采集 4、实时业务统计指标分析一——ODS分层设计与…...
对计网考研中的信道、传输时延、传播时延的理解
对计网考研中的信道、传输时延、传播时延的理解 在学习数据链路层流量控制和可靠传输那一节的三个协议的最大信道利用率时产生的疑惑 情景: 假如A主机和B主机通过集线器连接,A和集线器是光纤连接,B和集线器也是光纤连接,A给B发…...
RAGFlow报错:ESConnection.sql got exception
环境: Ragflowv0.17.2 问题描述: RAGFlow报错:ESConnection.sql got exception _ming_cheng_tks, 浙江, operatorOR;minimum_should_match30%) 2025-04-25 15:55:06,862 INFO 244867 POST http://localhost:1200/_sql?formatjson […...
报错:函数或变量 ‘calcmie‘ 无法识别。
1、具体报错 运行网上一个开源代码,但是运行报如下错: TT_para_gen 函数或变量 calcmie 无法识别。 出错 TT_para_gen>Mie (第 46 行) [S, C, ang,~] calcmie(rad, ns, nm, lambda, nang, ... 出错 TT_para_gen (第 17 行) [~,ang,Miee,C] …...
蓝桥杯获奖后心得体会
文章目录 获奖项备考心得📖 蓝桥杯 Java 研究生组备考心得📌 一、备考规划📌 二、考试技巧📌 三、心理调整📌 四、总结 获奖项 JAVA研究生组省二 备考心得 好!我来给你写一篇蓝桥杯研究生组Java方向的备…...
大鱼吃小鱼开源
因为知道一些学校也有相关C语言课设 所以这里对代码开源(如果没有csdn会员请用夸克) 我用夸克网盘分享了「大鱼吃小鱼-程序.zip」,点击链接即可保存。打开「夸克APP」,无需下载在线播放视频,畅享原画5倍速࿰…...
深度学习框架搭建(Vscode/Anaconda/CUDA/Pytroch)
目录 一 Vscode安装 二、Anaconda安装 三、更新显卡驱动 四、安装CUDA 五、安装Pytorch 六、Vscode配置 七、出现的问题汇总 一 Vscode安装 在 Windows 上安装 访问 VS Code 官网 https://code.visualstudio.com/,点击 "Downl…...
免费实用的图像处理工具箱
提到图像处理,一般会想到Photoshop。的确PS比较专业,但是学习成本比较高。其实,针对具体的应用场景,选些简单的工具软件会更方便。 今天就给大家分享一款“洋芋田图像工具箱”软件。它支持Windows和Mac电脑。 用这款软件&#x…...
范式演进:从ETL到ELT及未来展望
范式演进:从ETL到ELT及未来展望 引言 随着数据规模与实时分析需求的爆发,传统的集中式 ETL(Extract-Transform-Load)已难以为继,ELT(Extract-Load-Transform)凭借云原生仓库的弹性计算与分析工程的兴起逐步成为主流。2025 年,数据团队正加速从“先变换再加载”走向“…...
定义一个3D cube,并计算cube每个顶点的像素坐标
定义一个3D cube,并计算cube每个顶点的像素坐标 scratch a pixel课程:Your Starting Point! 3D场景中物体所有点与坐标系原点的连线,该连线与像素平面canvas的交点就是场景中3D点其投影点的位置 3D场景中的点均由这个坐标系描述 相似三角形ABC和A’B’…...
Python学习笔记(第二部分)
接续 Python.md 文件的第二部分 字典 字典的基本操作 字典是一系列 键—值 对,每一个键都与一个值关联,值可以是数字,字符串,甚至是列表或字典,可以把任何python对象都作为字典中的值 alien {color:green,points:5}键…...
ZYNQB笔记(十六):AXI DMA 环路测试
版本:Vivado2020.2(Vitis) 任务:使用 PL 端的 AXI DMA IP 核实现对 DDR3 中数据的读取与写入,实现数据环回,具体流程为: PS 端产生测试数据并写入到 DDR3 中,然后 PL 端的 AXI DMA I…...
Java学习手册:Spring 数据访问
一、Spring JDBC JdbcTemplate :Spring JDBC 提供了 JdbcTemplate 类,它简化了数据库操作,提供了丰富的 API 来执行数据库访问任务。JdbcTemplate 可以自动处理数据库连接的获取、释放,SQL 语句的执行,结果集的处理等…...
第二章 日志分析-apache日志分析(玄机系列)
简介 账号密码 root apacherizhi ssh rootIP 1、提交当天访问次数最多的IP,即黑客IP: 2、黑客使用的浏览器指纹是什么,提交指纹的md5: 3、查看包含index.php页面被访问的次数,提交次数: 4、查看黑客IP访问了…...
类比分析AI Agent 技术
引言:AI Agent 的本质与范式转变 在人工智能领域,AI Agent(智能体)代表了一种从传统软件系统到自主性实体的深刻范式转变。不同于仅仅执行预设指令或算法的程序,现代 AI Agent 被设计为能够: 感知 (Perce…...
【业务领域】PCIE协议理解
PCIE协议理解 提示:这里可以添加系列文章的所有文章的目录,目录需要自己手动添加 PCIE学习理解。 文章目录 PCIE协议理解[TOC](文章目录) 前言零、PCIE掌握点?一、PCIE是什么?二、PCIE协议总结物理层切速 链路层事务层6.2 TLP的路…...
设计模式简述(十四)组合模式
组合模式 描述基本使用所有节点方法一致使用 叶子无实现子节点使用 添加向上查询使用(没变化) 描述 组合模式用于描述部分与整体的关系,将个体对象与组合对象的行为统一,便于维护整个数据集。 基本使用 所有节点方法一致 定义…...
【Tool】vscode
vscode问题集锦 1 全局搜索失效 ctrl shift f 快捷键失效: 原因:可能与输入法快捷键冲突,重定义输入法快捷键即可 其他 看心情和经历补充~...
文件操作--文件包含漏洞
本文主要内容 脚本 ASP、PHP、JSP、ASPX、Python、Javaweb --# 各种包含函数 检测 白盒 代码审计 黑盒 漏扫工具、公开漏洞、手工看参数值及功能点 类型 本地包含 有限制、无限制 远程包含 无限制、有限制…...
数字智慧方案6156丨智慧医联体信息化解决方案(50页PPT)(文末有下载方式)
资料解读:智慧医联体信息化解决方案 详细资料请看本解读文章的最后内容。 在医疗改革不断深化的大背景下,医联体信息化建设成为推动医疗服务高质量发展的关键力量。《智慧医联体信息化解决方案》这份资料,全面且深入地阐述了医联体信息化建…...
华为eNSP:多区域集成IS-IS
一、什么是多区域集成IS-IS? 多区域集成IS-IS是一种基于中间系统到中间系统(IS-IS)协议优化的网络架构设计,通过多区域协同、路径优化和扩展性增强实现高效路由管理,其核心特征如下: 1、分布式架构与多区…...
RAG技术完全指南(一):检索增强生成原理与LLM对比分析
RAG技术完全指南(一):检索增强生成原理与LLM对比分析 文章目录 RAG技术完全指南(一):检索增强生成原理与LLM对比分析1. RAG 简介2. 核心思想3. 工作流程3.1 数据预处理(索引构建)3.2…...
(持续更新)Ubuntu搭建LNMP(Linux + Nginx + MySQL + PHP)环境
LNMP(Linux Nginx MySQL PHP)环境是在Linux操作系统上构建的一个高性能Web服务器环境。M也可以指代其他数据库,P也可以指代Python 1. 准备Linux系统 确保你已经在一台服务器或虚拟机上安装了Linux操作系统。推荐使用Ubuntu、CentOS或Debi…...
机器人手臂控制器:EMC电磁兼容解决(一)
一、机器人手臂控制器行业标准剖析 GB/T 39004—2020《工业机器人电磁兼容设计规范》 GB/T 37283-2019 服务机器人 电磁兼容 通用标准 抗扰度要求和限值 GB/T 39785-2021 服务机器人 机械安全评估与测试方法 GB/T 40014-2021 双臂工业机器人 性能及其试验方法 GB/T 40013-…...
Qt进阶开发:QSS常用的语法介绍和使用
文章目录 一、什么是QSS?二、QSS的基本语法三、QSS 的使用方式3.1 在代码中设置 QSS3.2 加载外部 QSS 文件四、QSS中选择器的介绍和使用4.1 Type Selector(类型选择器)4.2 ID Selector(ID 选择器)4.2.1 仅使用 ID(常见写法)4.2. 2 类型 + ID(更精确匹配)4.3 Class Sel…...
qemu学习笔记:QOM
2.4 QOM介绍 说明:小白学习qemu的一些学习笔记。主要是学习《QEMU&KVM源码解析与应用》这本书。 参考: 《QEMU&KVM源码解析与应用》作者:李强 Qemu - 百问网嵌入式Linux wiki QOM 定义:QEMU Object Model,是 Q…...
Spring AI开发跃迁指南(第二章:急速上手3——Advisor核心原理、源码讲解及使用实例)
1.Advisor简介 Spring AI 中的 Advisor 是一种核心机制,用于拦截和增强 AI 应用程序中的请求与响应流。其设计灵感来源于 Spring AOP(面向切面编程)中的切面(Aspect)概念,但专门针对 AI 交互场景进行了优化…...
51c嵌入式~单片机~合集9
我自己的原文哦~ https://blog.51cto.com/whaosoft/13884964 一、单片机中hex、bin文件的区别 单片机程序编译之后,除了生成hex文件之外还生成了bin文件,实际它们都是单片机的下载文件,下文介绍它们的区别。 Hex Hex文件包含地址信息。…...
linux学习——数据库API创建
一.API操作 1.int sqlite3_open(char *filename,sqlite3 **db) 功能:打开sqlite数据库 参数: filename:数据库文件路径 db:指向sqlite句柄的指针 (splite3* db;) 返回值…...
21.2Linux中的LCD驱动实验(驱动)_csdn
1、修改设备树 1.1、LCD 屏幕使用的 IO 配置 编译: make uImage LOADADDR0XC2000040 -j8 //编译内核复制给内核的镜像路径:1.2、LDTC 接口节点修改 1.3、输出接口的编写 2、在 panel-simple.c 文件里面添加屏幕参数 显示波浪线是因为alientek_desc 保存参…...
Dubbo(89)如何设计一个支持多语言的Dubbo服务?
设计一个支持多语言的Dubbo服务需要考虑以下几个方面: 服务接口设计:确保服务接口的定义可以被不同语言实现。序列化协议:选择一个支持多语言的序列化协议,例如Protobuf、Thrift、gRPC等。服务注册与发现:确保服务注册…...
油气地震资料数据中“照明”的含义
油气地震资料数据中“照明”的含义 在地震勘探中,“照明”(Illumination)是一个比喻性术语,用于描述地下地质构造被地震波能量覆盖的程度。其核心含义包括: 能量覆盖:指地震波(如人工激发的地…...
[FPGA Video IP] Frame Buffer Read and Write
Xilinx Video Frame Buffer Read and Write IP (PG278) 详细介绍 概述 Xilinx LogiCORE™ IP Video Frame Buffer Read(帧缓冲读取)和 Video Frame Buffer Write(帧缓冲写入)核(PG278)是一对专为视频处理…...
新能源行业供应链规划及集成计划报告(95页PPT)(文末有下载方式)
资料解读:《数字化供应链规划及集成计划现状评估报告》 详细资料请看本解读文章的最后内容。 该报告围绕新能源行业 XX 企业供应链展开,全面评估其现状,剖析存在的问题,并提出改进方向和关键举措,旨在提升供应链竞争力…...
curl详解
curl 是一个常用的命令行工具,用于发送 HTTP 请求,支持包括 GET、POST、PUT、DELETE 等在内的多种 HTTP 方法。它非常适合用来测试 API、下载文件、与后端服务进行交互等。接下来,我会详细讲解 curl 的基本用法以及常见的应用场景。 &#x…...
博客打卡-人类基因序列功能问题动态规划
题目如下: 众所周知,人类基因可以被认为是由4个核苷酸组成的序列,它们简单的由四个字母A、C、G和T表示。生物学家一直对识别人类基因和确定其功能感兴趣,因为这些可以用于诊断人类疾病和设计新药物。 生物学家确定新基因序列功能…...
Runnable组件动态添加默认调用参数
01. bind 函数用途与使用技巧 在使用 LangChain 开发时,某些场景我们希望在一个 Runnable 队列中调用另一个 Runnable 并传递常量参数,这些参数既非前序 Runnable 的输出,也不是用户输入,而是组件自身的部分参数。此时可以使用 R…...
系统架构设计师:设计模式概述
面向对象技术为软件技术带来新的发展。人们运用面向对象的思想分析系统、为系统建模并设计系统,最后使用面向对象的程序语言来实现系统。 但是面向对象的设计并不是一件很简单的事情,尤其是要设计出架构良好的软件系统更不容易。 为了提高系统的复用性…...
天山流域流量数据集(1991-2019)
时间分辨率日空间分辨率/共享方式开放获取数据大小131.67 MB数据时间范围 1901-01-01 — 2019-12-31 元数据更新时间2025-03-24 数据集摘要 由于天山地区数据稀缺和水文条件复杂,中亚水塔的自然径流数据集在各种全球径流数据集(如GMIS、GRDC)…...
Linux 环境下 Mysql 5.7 数据定期备份
目录 一、创建数据备份脚本二、查看备份日志三、数据库数据恢复 备份策略: 系统环境 openEuler 22.03 (LTS-SP4) 单机备份 每天凌晨2点,指定数据库表全量备份,只保留近7次备份数据 每次的脚本执行,将会记录执行结果到日志…...
多模态大语言模型arxiv论文略读(五十二)
M3D: Advancing 3D Medical Image Analysis with Multi-Modal Large Language Models ➡️ 论文标题:M3D: Advancing 3D Medical Image Analysis with Multi-Modal Large Language Models ➡️ 论文作者:Fan Bai, Yuxin Du, Tiejun Huang, Max Q. -H. M…...
REST API、FastAPI与Flask API的对比分析
以下是关于REST API、FastAPI与Flask API的对比分析,涵盖架构设计、性能表现、开发效率等核心维度: 一、核心定位与架构差异 REST API 本质:一种基于HTTP协议的架构风格,强调资源化操作(通过URI定位资源)、…...
【论文阅读26】贝叶斯-滑坡预测-不确定性
📖 这篇论文主要说了什么? 📌 背景: 滑坡预测里,预测失稳时间(Slope Failure Time, SFT) 很关键,但它受两方面不确定性影响: 观测不确定性(监测数据本身的…...
【笔记】深度学习模型训练的 GPU 内存优化之旅④:内存交换与重计算的联合优化篇
开设此专题,目的一是梳理文献,目的二是分享知识。因为笔者读研期间的研究方向是单卡上的显存优化,所以最初思考的专题名称是“显存突围:深度学习模型训练的 GPU 内存优化之旅”,英文缩写是 “MLSys_GPU_Memory_Opt”。…...
边缘计算革命:大模型轻量化部署全栈实战指南
当ResNet-152模型能在树莓派4B上实现每秒27帧实时推理时,边缘智能时代真正到来。本文解析从模型压缩到硬件加速的完整技术栈,实测Transformer类模型在移动端的部署时延可压缩至16ms,揭示ARM芯片实现INT4量化的工程秘诀与十种典型场景优化方案…...
LangChain4j +DeepSeek大模型应用开发——7 项目实战 创建硅谷小鹿
这部分我们实现硅谷小鹿的基本聊天功能,包含聊天记忆、聊天记忆持久化、提示词 1. 创建硅谷小鹿 创建XiaoLuAgent package com.ai.langchain4j.assistant;import dev.langchain4j.service.*; import dev.langchain4j.service.spring.AiService;import static dev…...
python自动化测试
Python自动化测试指南 Python是自动化测试领域的首选语言之一,凭借其简洁的语法、丰富的库和强大的生态系统,能够高效地实现各种测试需求。本文将详细介绍Python在自动化测试中的应用,涵盖Web测试、API测试、单元测试、GUI测试等多个方面。 1. 自动化测试基础 测试金字塔…...
49、【OS】【Nuttx】【OSTest】参数解析:测试项
背景 接之前 blog 48、【OS】【Nuttx】【OSTest】内存监控:分配释放推演 解析完内存监控,继续看下一个测试项 getopt_test 测试项 getopt_test 如下 getopt,getopt_long,getopt_long_only getopt() 用来解析命令行短选项&am…...