《ffplay 读线程与解码线程分析:从初始化到 seek 操作,对比视频与音频解码的差异》
1 read-thread
1.1 初始化部分
1.分配. avformat_alloc_context 创建上下⽂
ic = avformat_alloc_context();if (!ic) {av_log(NULL, AV_LOG_FATAL, "Could not allocate context.\n");ret = AVERROR(ENOMEM);goto fail;}
2 ic->interrupt_callback.callback = decode_interrupt_cb;
ic->interrupt_callback.opaque = is;//这个是设置参数
//特定选项处理if (!av_dict_get(format_opts, "scan_all_pmts", NULL, AV_DICT_MATCH_CASE)) {av_dict_set(&format_opts, "scan_all_pmts", "1", AV_DICT_DONT_OVERWRITE);scan_all_pmts_set = 1;}/* 3.打开文件,主要是探测协议类型,如果是网络文件则创建网络链接等 *///特定选项处理if (!av_dict_get(format_opts, "scan_all_pmts", NULL, AV_DICT_MATCH_CASE)) {av_dict_set(&format_opts, "scan_all_pmts", "1", AV_DICT_DONT_OVERWRITE);scan_all_pmts_set = 1;}/* 3.打开文件,主要是探测协议类型,如果是网络文件则创建网络链接等 */err = avformat_open_input(&ic, is->filename, is->iformat, &format_opts);if (err < 0) {print_error(is->filename, err);ret = -1;goto fail;}if (scan_all_pmts_set)av_dict_set(&format_opts, "scan_all_pmts", NULL, AV_DICT_MATCH_CASE);if ((t = av_dict_get(format_opts, "", NULL, AV_DICT_IGNORE_SUFFIX))) {av_log(NULL, AV_LOG_ERROR, "Option %s not found.\n", t->key);ret = AVERROR_OPTION_NOT_FOUND;goto fail;}is->ic = ic; // videoState的ic指向分配的ic
int av_dict_set(AVDictionary **pm, const char *key, const char *value, int flags);
4 avformat_find_stream_info(ic, opts);
if (find_stream_info) {AVDictionary **opts = setup_find_stream_info_opts(ic, codec_opts);int orig_nb_streams = ic->nb_streams;/** 4.探测媒体类型,可得到当前文件的封装格式,音视频编码参数等信息* 调用该函数后得多的参数信息会比只调用avformat_open_input更为详细,* 其本质上是去做了decdoe packet获取信息的工作* codecpar, filled by libavformat on stream creation or* in avformat_find_stream_info()*/err = avformat_find_stream_info(ic, opts);
该函数是通过读取媒体⽂件的部分数据来分析流信息。在⼀些缺少头信息的封装下特别有⽤,⽐如说MPEG(⾥应该说ts更准确)(FLV⽂件也是需要读取packet 分析流信息)。⽽被读取⽤以分析流信息的数据可能被缓存,供av_read_frame时使⽤,在播放时并不会跳过这部分packet的读取。
5 检测是否指定播放起始时间
/* 5. 检测是否指定播放起始时间 */if (start_time != AV_NOPTS_VALUE) {int64_t timestamp;timestamp = start_time;/* add the stream start time */if (ic->start_time != AV_NOPTS_VALUE)timestamp += ic->start_time;// seek的指定的位置开始播放ret = avformat_seek_file(ic, -1, INT64_MIN, timestamp, INT64_MAX, 0);if (ret < 0) {av_log(NULL, AV_LOG_WARNING, "%s: could not seek to position %0.3f\n",is->filename, (double)timestamp / AV_TIME_BASE);}}
6 查找查找AVStream
具体现在那个流进⾏播放我们有两种策略:
- 在播放起始指定对应的流
- 使⽤缺省的流进⾏播放
// 选择相关流
int avformat_match_stream_specifier(AVFormatContext *s, AVStream *st, const char *spec);//自动选择
av_find_best_stream
7 设置窗口大小
if (st_index[AVMEDIA_TYPE_VIDEO] >= 0) {AVStream *st = ic->streams[st_index[AVMEDIA_TYPE_VIDEO]];AVCodecParameters *codecpar = st->codecpar;//根据流和帧宽高比猜测视频帧的像素宽高比(像素的宽高比,注意不是图像的) //AVRational 是 FFmpeg 中用于精确表示分数的结构体AVRational sar = av_guess_sample_aspect_ratio(ic, st, NULL);if (codecpar->width) {// 设置显示窗口的大小和宽高比set_default_window_size(codecpar->width, codecpar->height, sar);}}
根据流和帧宽⾼⽐猜测帧的样本宽⾼⽐。
由于帧宽⾼⽐由解码器设置,但流宽⾼⽐由解复⽤器设置,因此这两者可能不相
等。
此函数会尝试返回待显示帧应当使⽤的宽⾼⽐值。
基本逻辑是优先使⽤流宽⾼⽐(前提是值是合理的),其次使⽤帧宽⾼⽐。
这样,流宽⾼⽐(容器设置,易于修改)可以覆盖帧宽⾼⽐。
8 stream_component_open
经过以上步骤,⽂件打开成功,且获取了流的基本信息,并选择⾳频流、视频流、字幕流。接下来就可以所选流对应的解码器了.
一共有两个选择 一是通过id 二是 通过name
decoder_init 初始化解码器
d->avctx = avctx; 绑定对应的解码器上下⽂
d->queue = queue; 绑定对应的packet队列
d->empty_queue_cond = empty_queue_cond; 绑定VideoState的continue_read_thread,当
解码线程没有packet可读时唤醒read_thread赶紧读取数据
d->start_pts = AV_NOPTS_VALUE; 初始化start_pts
d->pkt_serial = -1; 初始化pkt_serial
decoder_start启动解码器
packet_queue_start 启⽤对应的packet 队列
SDL_CreateThread 创建对应的解码线程
1.2 创建packet队列部分
开始
|
|-- 1. 检测是否退出
| |
| |-- 如果退出
| | |
| | |-- 退出程序
| |
| |-- 如果未退出
| |
| |-- 2. 检测是否暂停/继续
| |
| |-- 如果暂停
| | |
| | |-- 保持暂停状态,不进行后续数据读取和处理
| |
| |-- 如果继续
| |
| |-- 3. 检测是否需要seek
| |
| |-- 如果需要seek
| | |
| | |-- 执行seek操作,调整播放位置
| |
| |-- 如果不需要seek
| |
| |-- 4. 检测video是否为attached_pic
| |
| |-- 如果是attached_pic
| | |
| | |-- 进行attached_pic相关处理(具体处理方式依需求而定)
| |
| |-- 如果不是attached_pic
| |
| |-- 5. 检测队列是否已经有足够数据
| |
| |-- 如果队列数据不足
| | |
| | |-- 等待数据到达,不进行后续操作
| |
| |-- 如果队列有足够数据
| |
| |-- 6. 检测码流是否已经播放结束
| |
| |-- 如果码流播放结束
| | |
| | |-- a. 检查是否循环播放
| | |
| | |-- 如果循环播放
| | | |
| | | |-- 重置播放状态,回到起始位置重新播放
| | |
| | |-- 如果不循环播放
| | | |
| | | |-- b. 检查是否自动退出
| | | |
| | | |-- 如果自动退出
| | | | |
| | | | |-- 退出程序
| | | |
| | | |-- 如果不自动退出
| | | | |
| | | | |-- 保持当前状态(例如显示播放结束画面等)
| |
| |-- 如果码流未播放结束
| |
| |-- 7. 使用av_read_frame读取数据包
| |
| |-- 8. 检测数据是否读取完毕
| |
| |-- 如果数据读取完毕
| | |
| | |-- 进行数据读取完毕相关处理(如更新播放状态等)
| |
| |-- 如果数据未读取完毕
| |
| |-- 9. 检测是否在播放范围内
| |
| |-- 如果不在播放范围内
| | |
| | |-- 进行超出播放范围相关处理(如跳过数据等)
| |
| |-- 如果在播放范围内
| |
| |-- 10. 将数据插入对应的队列
| |
| |-- 继续后续播放处理(如解码、渲染等)
note1 attached_pic
在 FFmpeg 中,attached_pic 是一种特殊的视频帧,用于表示媒体文件的封面图片(如电影海报、音乐专辑封面)。这些图片通常作为媒体文件的一部分被嵌入,与视频流中的普通帧不同。检测视频是否为 attached_pic 的目的是识别并处理这类封面图片。
note 2 检测队列是否已经有⾜够数据
⾳频、视频、字幕队列都不是⽆限⼤的,如果不加以限制⼀直往队列放⼊packet,那将导致队列占⽤⼤量的内存空间,影响系统的性能,所以必须对队列的缓存⼤⼩进⾏控制。PacketQueue默认情况下会有⼤⼩限制,达到这个⼤⼩后,就需要等待10ms,以让消费者——解码线程能有时间消耗。
note 3 检测码流是否已经播放结束
PacketQueue和FrameQueue都消耗完毕,才是真正的播放完毕
note 4 检测数据是否读取完毕
是使用空包
note 5 检测是否在播放范围内
播放器可以设置:-ss 起始位置,以及 -t 播放时⻓
1.3 退出线程处理
- 如果解复⽤器有打开则关闭avformat_close_input
- 调⽤SDL_PushEvent发送退出事件FF_QUIT_EVENT
a. 发送的FF_QUIT_EVENT退出播放事件由event_loop()函数相应,收到FF_QUIT_EVENT后调⽤
do_exit()做退出操作。 - 消耗互斥量wait_mutex
1.4 核心要点
1 seek 怎么做:
当is->seek_req为真,即有 seek 请求时,会进行 seek 操作。首先计算seek_target、seek_min和seek_max的值,然后调用avformat_seek_file函数进行主要的 seek 操作。如果 seek 成功,需要清除 PacketQueue 的缓存,并放入一个flush_pkt,让 PacketQueue 的serial增 1,以区分 seek 前后的数据,同时该flush_pkt会触发解码器重新刷新解码器缓avcodec_flush_buffers(),避免解码出现马赛克。此外,还需要同步外部时钟。如果播放器本身处于 pause 状态,则执行step_to_next_frame(is),显示一帧后继续暂停。
int avformat_seek_file(AVFormatContext *s, int stream_index, int64_t min_ts, int64_t ts, int64_t max_ts, int flags);
时间戳三元组 (min_ts, ts, max_ts) //核心公式 时间戳 = 秒数 × 时间基
ts(目标时间戳)
你希望 seek 到的理想时间点(以时间基为单位)。
例如:如果你想 seek 到 30 秒位置,且时间基为 AV_TIME_BASE,则 ts = 30 * AV_TIME_BASE。
min_ts(最小时间戳)
seek 操作允许的最小时间边界。
FFmpeg 不会 seek 到小于此值的位置,即使 ts 比它小。
max_ts(最大时间戳)
seek 操作允许的最大时间边界。
FFmpeg 不会 seek 到大于此值的位置,即使 ts 比它大。
为什么需要范围?
媒体文件的 seek 通常只能定位到关键帧(I 帧),因为只有关键帧可以独立解码。
例如:你想 seek 到 30.5 秒,但最近的关键帧在 29.8 秒和 31.2 秒,此时:
如果允许向前搜索(AVSEEK_FLAG_BACKWARD 未设置),FFmpeg 可能选择 31.2 秒。
如果强制向后搜索(设置 AVSEEK_FLAG_BACKWARD),FFmpeg 会选择 29.8 秒。
通过设置 min_ts 和 max_ts,你可以约束 FFmpeg 的选择范围,避免 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;
// 前进seek seek_rel>0
//seek_min = seek_target - is->seek_rel + 2;
//seek_max = INT64_MAX;
// 后退seek seek_rel<0
//seek_min = INT64_MIN;
//seek_max = seek_target + |seek_rel| -2;
//seek_rel =0 鼠标直接seek
//seek_min = INT64_MIN;
//seek_max = INT64_MAX;
2 数据播放完毕和码流数据读取完毕
- 数据播放完毕:判断播放已完成需要同时满足多个条件,包括不在暂停状态;音频未打开,或者打开了但解码已解完所有 packet,自定义的解码器serial等于 PacketQueue 的serial,并且 FrameQueue 中没有数据帧;视频未打开,或者打开了但解码已解完所有 packet,自定义的解码器serial等于 PacketQueue 的serial,并且 FrameQueue 中没有数据帧。只有 PacketQueue 和 FrameQueue 都消耗完毕,才是真正的播放完毕。
- 码流数据读取完毕:当av_read_frame读取数据返回值ret < 0,并且(ret == AVERROR_EOF || avio_feof(ic->pb))且!is->eof时,表示码流数据读取完毕,此时会向对应音频、视频、字幕队列插入 “空包”,通知解码器冲刷 buffer,将缓存的所有数据都解出来并取出。
3 循环播放:在确认码流已播放结束的情况下,如果loop变量控制循环播放,当loop不等于 1(loop为 0 表示无限次循环,减 1 后大于 0 也允许循环 ),则将文件 seek 到起始位置,起始位置不一定是从头开始,具体要看用户是否指定了起始播放位置,通过stream_seek(is, start_time != AV_NOPTS_VALUE ? start_time : 0, 0, 0);实现。
4 指定播放位置:可以通过ffplay -ss设置起始时间来指定播放位置,时间格式为hh:mm:ss,例如ffplay -ss 00:00:30 test.flv则是从 30 秒的起始位置开始播放。在代码中,检测是否指定播放起始时间,如果指定时间则通过avformat_seek_file函数 seek 到指定位置。
2 音视频解码线程分析
ffplay的解码线程独⽴于数据读线程,并且每种类型的流(AVStream)都有其各⾃的解码线程,如:
video_thread⽤于解码video stream;
audio_thread⽤于解码audio stream;
subtitle_thread⽤于解码subtitle stream。
解码器相关函数(decoder 为 ffplay 自定义、重新封装,avcodec 由 ffmpeg 提供)
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 time
AVRational start_pts_tb; // 初始化时是stream的time_base
int64_t next_pts; // 记录最近⼀次解码后的frame的pts,当解出来的部分帧没有有效的pts
时则使⽤next_pts进⾏推算
AVRational next_pts_tb; // next_pts的单位
SDL_Thread *decoder_tid; // 线程句柄
} Decoder;
api
- 初始化解码器
函数:void decoder_init(Decoder *d, AVCodecContext *avctx, PacketQueue *queue, SDL_cond *empty_queue_cond); - 启动解码器
函数:int decoder_start(Decoder *d, int (*fn)(void *), const char thread_name, void arg) - 解帧
函数:int decoder_decode_frame(Decoder *d, AVFrame *frame, AVSubtitle *sub); - 终止解码器
函数:void decoder_abort(Decoder *d, FrameQueue *fq); - 销毁解码器
函数:void decoder_destroy(Decoder *d);
使用方法
启动解码线程
调用 decoder_init()
调用 decoder_start()
2.1 视频解码
static int video_thread(void *arg)
{VideoState *is = arg;AVFrame *frame = av_frame_alloc(); // 分配解码帧double pts; // ptsdouble duration; // 帧持续时间int ret;//1 获取stream timebaseAVRational tb = is->video_st->time_base; // 获取stream timebase//2 获取帧率,以便计算每帧picture的durationAVRational frame_rate = av_guess_frame_rate(is->ic, is->video_st, NULL);if (!frame)return AVERROR(ENOMEM);for (;;) { // 循环取出视频解码的帧数据// 3 获取解码后的视频帧ret = get_video_frame(is, frame);if (ret < 0)goto the_end; //解码结束, 什么时候会结束if (!ret) //没有解码得到画面, 什么情况下会得不到解后的帧continue;
AVRational av_guess_frame_rate(AVFormatContext *ctx, AVStream *stream, AVFrame *frame);
猜测帧率
线程的总体流程很清晰:
- 获取stream timebase,以便将frame的pts转成秒为单位
- 获取帧率,以便计算每帧picture的duration
- 获取解码后的视频帧,具体调⽤get_video_frame()实现
- 计算帧持续时间和换算pts值为秒
- 将解码后的视频帧插⼊队列,具体调⽤queue_picture()实现
- 释放frame对应的数据
get_video_frame()
static int get_video_frame(VideoState *is, AVFrame *frame)
{int got_picture;// 1. 获取解码后的视频帧if ((got_picture = decoder_decode_frame(&is->viddec, frame, NULL)) < 0) {return -1; // 返回-1意味着要退出解码线程, 所以要分析decoder_decode_frame什么情况下返回-1}if (got_picture) {// 2. 分析获取到的该帧是否要drop掉, 该机制的目的是在放入帧队列前先drop掉过时的视频帧double dpts = NAN;if (frame->pts != AV_NOPTS_VALUE)dpts = av_q2d(is->video_st->time_base) * frame->pts; //计算出秒为单位的ptsframe->sample_aspect_ratio = av_guess_sample_aspect_ratio(is->ic, is->video_st, frame);if (framedrop>0 || // 允许drop帧(framedrop && get_master_sync_type(is) != AV_SYNC_VIDEO_MASTER))//非视频同步模式{if (frame->pts != AV_NOPTS_VALUE) { // pts值有效double diff = dpts - get_master_clock(is);if (!isnan(diff) && // 差值有效fabs(diff) < AV_NOSYNC_THRESHOLD && // 差值在可同步范围呢diff - is->frame_last_filter_delay < 0 && // 和过滤器有关系is->viddec.pkt_serial == is->vidclk.serial && // 同一序列的包is->videoq.nb_packets) { // packet队列至少有1帧数据is->frame_drops_early++;printf("%s(%d) diff:%lfs, drop frame, drops:%d\n",__FUNCTION__, __LINE__, diff, is->frame_drops_early);av_frame_unref(frame);got_picture = 0;}}}}return got_picture;
}
- 调⽤ decoder_decode_frame 解码并获取解码后的视频帧;
- 分析如果获取到帧是否需要drop掉(逻辑就是如果刚解出来就落后主时钟,那就没有必要放⼊Frame队列)
decoder_decode_frame
- 持续获取解码帧(处理多帧情况)
解码器通过循环调用 avcodec_receive_frame() 获取解码后的帧,确保处理以下情况:
-
多帧解码:单个 Packet 可能解码出多个 Frame(如 B 帧序列),需循环读取直至返回 AVERROR(EAGAIN)。
-
流连续性校验:每次读取前检查 d->queue->serial == d->pkt_serial,确保处理的是同一播放序列的连续数据。若序列不一致,说明发生了 seek 或其他中断,需丢弃旧数据。
2. 获取新 Packet 并处理序列变更
通过 packet_queue_get() 获取新的 Packet,该操作可能阻塞等待数据: -
序列校验:若获取的 Packet 的 serial 与当前解码器的 pkt_serial 不一致,说明数据流已中断(如 seek 后),需丢弃该 Packet 并继续获取。
-
空队列通知:当 PacketQueue 为空时,发送 empty_queue_cond 条件信号,唤醒读线程继续读取数据(对应 read_thread 中的 SDL_CondWait())。
- 提交 Packet 到解码器
将校验后的 Packet 通过 avcodec_send_packet() 提交至解码器,触发解码流程:
错误处理:若发送失败(如解码器资源不足),需释放 Packet 并等待下次机会。
状态更新:成功发送后,更新解码器的 pkt_serial 为当前 Packet 的 serial,确保后续帧校验一致性。
queue_picture()
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);
#endifif (!(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);av_frame_move_ref(vp->frame, src_frame); // 将src中所有数据转移到dst中,并复位src。frame_queue_push(&is->pictq); // 更新写索引位置return 0;
}
queue_picture 的代码很直观:
⾸先 frame_queue_peek_writable 取FrameQueue的当前写节点;
然后把该拷⻉的拷⻉给节点(struct Frame)保存
再 frame_queue_push ,“push”节点到队列中。唯⼀需要关注的是,AVFrame的拷⻉是通过
av_frame_move_ref 实现的,所以拷⻉后 src_frame 就是⽆效的了
2.2 视频解码流程 要点分析
1 . flush_pkt的作⽤
强制解码器输出所有缓冲帧
当调用 avcodec_send_packet(NULL) 时,解码器会认为输入流已结束,并将内部缓存的所有待输出帧(如 B 帧队列)全部输出。
在 seek 或切换流时重置解码器状态
当用户执行 seek 或切换音视频流时,需要丢弃解码器中旧的缓冲数据,避免显示过时帧:
2 Decoder的packet_pending和pkt的作⽤
pkt
- 作用:存储当前待发送给解码器的 AVPacket。
- 生命周期:
从 PacketQueue 中获取一个新的 AVPacket。
通过 avcodec_send_packet() 发送给解码器。
发送成功后,释放该 AVPacket。
packet_pending
- 作用:标记 pkt 中是否有未发送的数据包。
- 应用场景:
当 avcodec_send_packet() 因解码器临时繁忙(返回 AVERROR(EAGAIN))而失败时,将 packet_pending 置为真,保留当前 pkt 以便下次重试。例如:
3 解码流程:avcodec_receive_frame-> packet_queue_get-> avcodec_send_packet
开始
│
├── 1. 检查解码器是否有剩余帧
│ │
│ ├── 调用 avcodec_receive_frame()
│ │ ├── 成功(返回0)→ 处理帧并重复步骤1
│ │ └── 失败(返回AVERROR(EAGAIN))→ 继续步骤2
│
├── 2. 获取新 Packet
│ │
│ ├── 检查 serial 是否匹配(是否发生 seek)
│ │ ├── 不匹配 → 丢弃 Packet 并重复步骤2
│ │ └── 匹配 → 继续步骤3
│
└── 3. 发送 Packet 到解码器
│
└── 回到步骤1
设计意图
- 最大化解码效率:
通过优先调用 avcodec_receive_frame(),确保解码器缓冲区中的所有帧都被及时处理,避免积压。- 应对多帧解码:
一个 Packet 可能解码出多个 Frame(如 H.264 的 B 帧组),循环调用- receive_frame 可确保处理所有帧。
处理解码器延迟:
某些编解码器(如 AAC、H.264)有内部延迟,即使没有新 Packet 输入,仍可能输出残留帧。
2.3 ⾳频解码线程
static int audio_thread(void *arg)
{VideoState *is = arg;AVFrame *frame = av_frame_alloc(); // 分配解码帧Frame *af;int got_frame = 0; // 是否读取到帧AVRational tb; // timebaseint ret = 0;if (!frame)return AVERROR(ENOMEM);do {// 1. 读取解码帧if ((got_frame = decoder_decode_frame(&is->auddec, frame, NULL)) < 0)goto the_end;if (got_frame) {tb = (AVRational){1, frame->sample_rate}; // 设置为sample_rate为timebase// 2. 获取可写Frameif (!(af = frame_queue_peek_writable(&is->sampq))) // 获取可写帧goto the_end;// 3. 设置Frame并放入FrameQueueaf->pts = (frame->pts == AV_NOPTS_VALUE) ? NAN : frame->pts * av_q2d(tb);af->pos = frame->pkt_pos;af->serial = is->auddec.pkt_serial;af->duration = av_q2d((AVRational){frame->nb_samples, frame->sample_rate});av_frame_move_ref(af->frame, frame);frame_queue_push(&is->sampq);#if CONFIG_AVFILTERif (is->audioq.serial != is->auddec.pkt_serial)break;}if (ret == AVERROR_EOF) // 检查解码是否已经结束,解码结束返回0is->auddec.finished = is->auddec.pkt_serial;
#endif}} while (ret >= 0 || ret == AVERROR(EAGAIN) || ret == AVERROR_EOF);
the_end:
#if CONFIG_AVFILTERavfilter_graph_free(&is->agraph);
#endifav_frame_free(&frame);return ret;
}
差异一:
在 ffplay 中,视频线程(video_thread()
)和音频线程对时间基(timebase)的处理确实存在差异,这主要源于音视频同步的复杂性和不同的处理逻辑。以下是详细解释:
- 视频线程(
video_thread()
)使用stream->time_base
的原因**
视频帧的时间戳(PTS)直接决定了其显示时间,因此:
- 精确同步:视频线程需要基于原始流的时间基计算每一帧的显示时间(
pts * stream->time_base
),以确保与音频同步。 - 帧显示逻辑:视频帧的显示时间(
vp->pts
)在入队前已转换为stream->time_base
,因此解码后直接使用该时间基进行同步计算。
示例代码:
// 视频帧入队前的时间戳转换
vp->pts = av_frame_get_best_effort_timestamp(frame);
vp->pts *= av_q2d(stream->time_base); // 转换为秒
- 音频线程不直接使用
stream->time_base
的原因**
音频同步更为复杂,主要依赖于以下因素:
- 音频时钟(
audio_clock
):音频播放的实时进度,以AV_TIME_BASE
为单位。 - 音频缓冲区:音频数据通常以块(packet)形式解码,但播放时需要连续的样本流,因此需要更精细的时间控制。
- 重采样需求:音频可能需要重采样以匹配输出设备的采样率,这会改变实际播放时间。
处理逻辑:
- 音频线程解码后,计算音频帧的结束时间(
audio_clock
),单位为AV_TIME_BASE
。 - 视频同步时,通过比较视频帧的 PTS(基于
stream->time_base
)与audio_clock
(基于AV_TIME_BASE
)来调整显示时机。
示例代码:
// 更新音频时钟(单位:AV_TIME_BASE)
is->audio_clock = af->pts + (double)af->frame->nb_samples / af->frame->sample_rate;
is->audio_clock *= AV_TIME_BASE; // 转换为 AV_TIME_BASE 单位
-
关键差异总结**
| 维度 | 视频线程 | 音频线程 |
|----------------|----------------------------------|----------------------------------|
| 时间基 |stream->time_base
|AV_TIME_BASE
|
| 核心目标 | 帧精确显示时间 | 连续播放与实时时钟维护 |
| 同步方式 | 基于视频 PTS 与音频时钟比较 | 基于音频缓冲区和系统时钟 |
| 复杂度 | 较低(直接显示) | 较高(重采样、缓冲区管理) | -
为什么设计不同?
- 音频连续性:音频播放需要保持连续性,任何微小的时间误差都会导致卡顿或变调,因此使用更稳定的全局时间基。
- 视频灵活性:视频帧可以通过丢帧、重复帧等方式调整显示时间,对时间基的精度要求相对较低。
- 简化计算:音频时钟作为主时钟(默认模式),使用
AV_TIME_BASE
可以避免频繁的时间基转换,提高性能。
- 总结
视频线程使用stream->time_base
是为了直接处理原始视频帧的显示时间,而音频线程使用AV_TIME_BASE
是为了维护稳定的主时钟,确保音视频同步的精确性和连续性。这种差异是基于音视频特性和同步需求的优化设计。
视频解码线程和音频解码线程在ffplay中确实存在一些关键差异,除了时间基的不同外,主要体现在以下几个方面:
总结对比表
特性 | 视频解码线程 | 音频解码线程 |
---|---|---|
解码API | 封装函数 get_video_frame() | 直接调用 decoder_decode_frame() |
持续时间计算 | 基于帧率(av_guess_frame_rate() ) | 基于采样率和样本数 |
同步策略 | 丢帧/重复帧 | 硬件时钟驱动播放 |
缓冲区大小 | 较小(3帧) | 较大(9帧) |
错误容忍度 | 较低(遇到错误易退出) | 较高(持续尝试解码) |
后处理 | 格式转换(YUV→RGB) | 重采样(适配输出设备) |
核心差异原因
-
实时性要求:
音频对实时性要求更高,任何延迟或卡顿都会明显影响体验,因此采用更简单直接的处理流程。 -
视觉容忍度:
人眼对视频的短暂丢帧或重复帧相对不敏感,因此视频线程可以更灵活地调整同步策略。 -
数据特性:
音频数据是连续的流式数据,而视频数据是离散的帧,处理逻辑天然不同。
2.4 音视频滤镜
在 ffplay 中,视频和音频滤镜的处理差异源于其数据特性和应用场景的不同。以下是两者的核心差异:
1. 默认启用状态
-
视频滤镜:
默认启用,即使未指定滤镜参数,也会通过 空滤镜(null filter) 确保数据一致性。
目的:统一处理流程,简化代码逻辑。 -
音频滤镜:
默认禁用,需通过命令行参数(如-af "volume=0.5"
)显式启用。
原因:音频重采样通常由解码器直接处理,无需额外滤镜。
2. 滤镜配置与初始化
视频滤镜
- 通过
configure_video_filters()
配置,依赖avfilter_graph_parse2()
构建滤镜图。 - 必须包含
buffer
(输入)和buffersink
(输出)两个特殊滤镜。 - 示例配置:
// 默认空滤镜配置 const char *filters = "null";
音频滤镜
- 通过
configure_audio_filters()
配置,需指定输入/输出格式和采样率。 - 更复杂,可能涉及多阶段处理(如重采样、效果调整)。
- 示例配置:
// 带重采样的滤镜链 "aresample=44100,volume=0.5"
3. 时间基处理
-
视频:
滤镜处理后,时间基通常保持为stream->time_base
,与原始视频流一致。 -
音频:
滤镜可能改变采样率,导致时间基变化,需重新计算 PTS。tb = (AVRational){1, frame->sample_rate}; // 基于新采样率的时间基
4. 处理流程与API调用
视频线程
- 通过
get_video_frame()
获取解码帧。 - 帧数据流经滤镜图,输出处理后的帧。
- 处理后的帧入队等待显示。
// 视频滤镜处理流程
ret = get_video_frame(is, frame); // 内部包含滤镜处理
if (ret > 0) {// 处理后的帧入队ret = queue_picture(is, frame, ...);
}
音频线程
- 解码后的数据直接入队,不经过滤镜。
- 音频播放时,通过
audio_decode_frame()
动态应用滤镜(如需要)。
// 音频滤镜处理流程(简化)
if (got_frame) {// 解码帧直接入队af->frame = frame;frame_queue_push(&is->sampq);
}// 音频播放时应用滤镜(在 audio_decode_frame() 中)
if (is->agraph) {// 滤镜处理av_buffersrc_add_frame_flags(...)av_buffersink_get_frame(...)
}
5. 性能与延迟
-
视频滤镜:
可能引入显著延迟(如复杂的缩放或特效),但人眼对视频延迟容忍度较高。 -
音频滤镜:
对实时性要求极高,复杂滤镜可能导致卡顿,因此默认保持最简处理。
6. 典型应用场景
场景 | 视频滤镜 | 音频滤镜 |
---|---|---|
格式转换 | 缩放、色彩空间转换(如 YUV→RGB) | 重采样(如 48kHz→44.1kHz) |
效果调整 | 亮度/对比度、裁剪、去噪 | 音量调节、均衡器、3D音效 |
默认行为 | 强制通过空滤镜 | 仅在显式请求时启用 |
总结对比表
特性 | 视频滤镜 | 音频滤镜 |
---|---|---|
默认启用 | 是(含空滤镜) | 否(需显式配置) |
配置函数 | configure_video_filters() | configure_audio_filters() |
处理阶段 | 解码后立即处理 | 播放时动态处理 |
时间基影响 | 通常不变 | 可能因重采样改变 |
延迟容忍度 | 较高 | 极低(需实时播放) |
典型滤镜链 | buffer → scale → buffersink | abuffer → aresample → volume |
核心差异原因
-
数据特性:
视频是离散帧,可容忍一定延迟;音频是连续流,对实时性要求极高。 -
处理复杂度:
视频滤镜通常更复杂(如画面缩放),而音频滤镜多为线性处理(如重采样)。
相关文章:
《ffplay 读线程与解码线程分析:从初始化到 seek 操作,对比视频与音频解码的差异》
1 read-thread 1.1 初始化部分 1.分配. avformat_alloc_context 创建上下⽂ ic avformat_alloc_context();if (!ic) {av_log(NULL, AV_LOG_FATAL, "Could not allocate context.\n");ret AVERROR(ENOMEM);goto fail;}2 ic->interrupt_callback.callback deco…...
AI+可视化:数据呈现的未来形态
当AI生成的图表开始自动“美化”数据,当动态可视化报告能像人类一样“讲故事”,当你的眼球运动直接决定数据呈现方式——数据可视化的未来形态,正在撕裂传统认知。某车企用AI生成的3D可视化方案,让设计师集体失业;某医…...
Spring Boot 整合 Redis 实战
一、整合准备:环境与依赖 1. 技术栈说明 Spring Boot 版本:3.1.2(兼容 Java 17) Redis 服务器:Redis 7.0(本地部署或 Docker 容器) Maven 依赖: <dependency><…...
pdf url 转 图片
背景:vue2.0需要把pdf转成图片,显示在url里面,使用pdfjs-dist来解决 步骤: 1、安装依赖包(我的项目是node12,安装太高版本会报错) npm i pdfjs-dist2.16.105 2、vue代码 <template><div class"main…...
JPG与PDF格式转换器
该插件可实现JPG与PDF格式的互转。 MainForm.Designer.cs using System.Windows.Forms; namespace JpgToPdfConverter {partial class MainForm{private System.ComponentModel.IContainer components null;protected override void Dispose(bool disposing){if (disposing &…...
Kafka 如何保证消息顺序性
文章目录 分区策略轮询随机按 Partition 路由按 key 路由 顺序性保证 分区策略 Kafka 的消息会被生产者发送到分区 Partition中,然后消费者通过获取分区中的消息来完成消费操作。所以Kafka不仅有主题 Topic这个概念,也引入分区 Partition 这个概念来实现…...
C++23 views::zip 和 views::zip_transform (P2321R2) 深入解析
文章目录 一、引言二、C23与Ranges库背景知识2.1 C23概述2.2 Ranges库回顾 三、views::zip 详解3.1 功能与定义3.2 使用场景3.3 示例代码 四、views::zip_transform 详解4.1 功能与定义4.2 使用场景4.3 示例代码 五、views::zip 与 views::zip_transform 的对比5.1 功能差异5.2…...
Starrocks的主键表涉及到的MOR Delete+Insert更新策略
背景 写这个文章的作用主要是做一些总结和梳理,特别是正对大数据场景下的实时写入更新策略 COW 和 MOR 以及 DeleteInsert 的技术策略的演进, 这也适用于其他大数据的计算存储系统。该文章主要参考了Primary Key table. 分析总结 Starrocks 的主键表主…...
【计算机视觉】OpenCV实战项目:基于OpenCV的车牌识别系统深度解析
基于OpenCV的车牌识别系统深度解析 1. 项目概述2. 技术原理与算法设计2.1 图像预处理1) 自适应光照补偿2) 边缘增强 2.2 车牌定位1) 颜色空间筛选2) 形态学操作3) 轮廓分析 2.3 字符分割1) 投影分析2) 连通域筛选 2.4 字符识别 3. 实战部署指南3.1 环境配置3.2 项目代码解析 4.…...
20250513_问题:由于全局Pytorch导致的错误
文章目录 前言问题:由于全局Pytorch导致的错误找到问题(1)找到问题(2) 总结 前言 问题:由于全局Pytorch导致的错误 代码报错显示无法使用CUDA AttributeError: module ‘torch._C’ has no attribute ‘_cuda_setDevice’ 找到问题(1) import torch pri…...
flinksql实践(从kafka读数据)
本案例是基于flinksql实现的,将逐步实现从kafka读写数据,聚合查询,关联维表(外部系统)等。 环境准备 首先确保电脑已经安装好zookeeper、kafka、flink。本文flink使用单机模式,zookeeper和kafka也使用单机配置。(环境配置部分可以…...
Linux系统:文件系统前言,详解CHSLBA地址
本节重点 理解硬盘的组成结构与工作原理理解柱面、扇区CHS地址进行寻址对整块硬盘的逻辑抽象LBA地址进行寻址LBA与CHS地址的相互转换 一、初识硬盘 1.1 基本概念 硬盘(Hard Disk Drive,HDD)是一种用于存储和检索数据的计算机硬件设备。它…...
2025年01月10日浙江鑫越系统科技前端面试
目录 vue2 和 vue3 的区别vue 怎么封装组件js 怎么把一个数组置空怎么组件自己调用自己的组件v-bind:attribute 和 v-bind“{attribute}” 的区别var let const 的区别this 指向作用域链闭包原型链事件循环 1. vue2 和 vue3 的区别 Vue 2 和 Vue 3 在多个方面存在区别&#…...
2025.05.11阿里云机考真题算法岗-第三题
📌 点击直达笔试专栏 👉《大厂笔试突围》 💻 春秋招笔试突围在线OJ 👉 笔试突围OJ 03. 镜像追踪游戏 问题描述 A先生正在玩一款名为「镜像追踪」的游戏。游戏在一个 n n n...
实景三维建模软件应用场景(众趣科技实景三维建模)
实景三维建模软件应用场景概述 实景三维建模软件,作为数字化时代的重要工具,不仅能够真实、立体、时序化地反映和表达物理世界,还为国家的基础设施建设和数字化发展提供了有力的支撑。 在测绘与地理信息领域,实景三维建模软件是构…...
centos中libc.so.6No such file的解决方式
你在运行安装程序时遇到了以下错误: Configuring the installer for this systems environment...strings: /lib/libc.so.6: No such file 这个错误通常是由于系统中缺少 glibc(GNU C Library)或其相关文件导致的。glibc 是 Linux 系统中的…...
MapReduce打包运行
1. 编写 MapReduce 程序 首先需要编写 MapReduce 程序,通常包含 Mapper、Reducer 和 Driver 类。例如,一个简单的 WordCount 程序: java import java.io.IOException; import java.util.StringTokenizer;import org.apache.hadoop.conf.Con…...
RustDesk:开源电脑远程控制软件
RustDesk:开源电脑远程控制软件 RustDesk:开源电脑远程控制软件一、RustDesk 简介二、下载教程2.1 桌面版下载2.2 Android 版下载 三、安装教程3.1 桌面版安装 四、功能讲解4.1 远程控制4.2 文件传输4.3 安全可靠4.4 自定义服务器 五、RustDesk技术架构解…...
【Unity】WebGL开发问题汇总
1 前言 主要记录下WebGL开发过程中遇到的各种问题。 2 问题 2.1 中文字体不显示 问题: 经典问题了。将项目打包在WebGL平台后,运行发现所用中文字体都不现实了。 解决办法: 替换Text组件的“字体”。可以将电脑字体复制到项目当中然后替换组…...
华为海思系列----昇腾张量编译器(ATC)模型转换工具----入门级使用指南(LINUX版)
由于官方SDK比较冗余且经常跨文档讲解且SDK整理的乱七八糟,对于新手来说全部看完上手成本较高,本文旨在以简短的方式介绍 CAFFE / ONNX 模型转 om 模型,并进行推理的全流程。希望能够帮助到第一次接触华为海思框架的道友们。大佬们就没必要看这种基础文章啦! 注:本…...
c++STL-list的模拟实现
cSTL-list的模拟实现 list源码剖析list模拟实现list构造函数拷贝构造函数赋值重载迭代器 iterator访问结点数size和判空尾插 push_back头插 push_front尾删pop_back头删pop_front插入 insert删除 erase清空clear和析构函数访问结点 参考程序 list源码剖析 建议先看cSTL-list的…...
RabbitMQ 核心概念与消息模型深度解析(二)
四、代码实战 了解了 RabbitMQ 的核心概念和消息模型后,接下来我们通过代码实战来进一步加深对它们的理解和掌握。下面将以 Java 和 Spring AMQP 为例,展示如何使用 RabbitMQ 进行消息的发送和接收。 4.1 环境准备 在开始编写代码之前,需要…...
JAVA研发+前后端分离,ZKmall开源商城B2C商城如何保障系统性能?
在电商行业竞争白热化的当下,B2C 商城系统的性能表现成为决定用户留存与商业成败的关键因素。ZKmall 开源商城凭借 Java 研发与前后端分离架构的深度融合,构建起一套高效、稳定且具备强大扩展性的系统架构,从底层技术到上层应用全方位保障性能…...
【android bluetooth 框架分析 02】【Module详解 6】【StorageModule 模块介绍】
1. 背景 我们在 gd_shim_module 介绍章节中,看到 我们将 StorageModule 模块加入到了 modules 中。 // system/main/shim/stack.cc modules.add<storage::StorageModule>();在 ModuleRegistry::Start 函数中我们对 加入的所有 module 挨个初始化。 而在该函…...
Datawhale 5月llm-universe 第1次笔记
课程地址:GitHub - datawhalechina/llm-universe: 本项目是一个面向小白开发者的大模型应用开发教程,在线阅读地址:https://datawhalechina.github.io/llm-universe/ 难点:配置conda环境变量 我用的vscode github方法 目录 重要…...
Linux架构篇、第五章git2.49.0部署与使用
Linux_架构篇 欢迎来到Linux的世界,看笔记好好学多敲多打,每个人都是大神! 题目:git2.49.0部署与使用 版本号: 1.0,0 作者: 老王要学习 日期: 2025.05.13 适用环境: Centos7 文档说明 这份文档聚焦于在 CentOS 7 环境下部署和…...
南方科技大学Science! 自由基不对称催化新突破 | 乐研试剂
近日,南方科技大学刘心元教授团队联合浙江大学洪鑫教授团队在自由基不对称催化领域取得新进展。课题组开发了一系列大位阻阴离子 N,N,P-配体,用于铜催化未活化外消旋仲烷基碘与亚砜亚胺的不对称胺化反应。该反应表现出广泛的底物兼容性,涵盖具…...
手机换IP真的有用吗?可以干什么?
在当今数字化时代,网络安全和个人隐私保护日益受到重视。手机作为我们日常生活中不可或缺的工具,其网络活动痕迹往往通过IP地址被记录和追踪。那么,手机换IP真的有用吗?它能为我们带来哪些实际好处?本文将为你一一解答…...
【C++详解】类和对象(上)类的定义、实例化、this指针
文章目录 一、类的定义1、类定义格式2、访问限定符3、类域 二、实例化1、实例化概念2、对象大小 三、this指针 一、类的定义 1、类定义格式 class为定义类的关键字,Stack为类的名字,{}中为类的主体,注意类定义结束时后面分号不能省略。类体中…...
C语言—再学习(数据的存储类别)
在c语言中,每个变量和函数都有两个属性:数据类型和数据的存储类别 C的存储类别包括4种:自动挡(auto)、静态的(static)、寄存器的(register)、外部的(extern&…...
软考软件评测师——计算机组成与体系结构(分级存储架构)
一、虚拟存储技术 虚拟存储系统通过软硬件协同实现内存扩展,其核心特征包括: 逻辑容量扩展能力:实际物理内存与外存结合,呈现远大于物理内存的连续地址空间动态加载机制:程序运行时仅加载必要部分到内存,…...
需求跟踪矩阵准确性的5大策略
需求跟踪矩阵的准确性可显著提升软件项目质量,确保需求的全面覆盖、减少遗漏和偏差,有利于优化变更管理,降低返工风险,最终保障产品符合用户预期和业务目标。如果不能保证跟踪矩阵的准确性,可能会导致需求遗漏、测试覆…...
【调度算法】MAPF多智能体路径规划问题
参考链接:https://blog.csdn.net/qq_43353179/article/details/129396325 在这篇博客的基础上对一些省略的部分进行补充。 网站:https://mapf.info/ 可行性判断 1. k-鲁棒性(k-robust MAPF) 在经典 MAPF 中,只要所有…...
迅龙3号基于兆讯MH22D3适配CST328多点触摸驱动开发笔记
MH22D3芯片是兆讯公司新推出的基于cortex-M3内核的新一代芯片,专注于显示应用,其主频高达216Mhz,64KB SRAM,512KB Flash,开发UI应用游刃有余。详细介绍请看:MH22D3新一代显控应用性价比之王 新龙微基于MH22…...
推荐算法工程化:ZKmall模板商城的B2C 商城的用户分层推荐策略
在 B2C 电商竞争激烈的市场环境中,精准推荐已成为提升用户体验、促进商品销售的关键。ZKmall 模板商城通过推荐算法工程化手段,深度挖掘用户数据价值,制定科学的用户分层推荐策略,实现 “千人千面” 的个性化推荐,帮助…...
你对于JVM底层的理解
JVM(Java虚拟机)是一个执行Java字节码的虚拟机,负责将Java程序的代码转化为能够在不同操作系统上运行的机器码。为了深入理解JVM的底层工作原理,可以从以下几个方面入手: 1. 类加载机制 JVM的类加载机制是其核心之一…...
深入探讨 Java 性能术语与优化实践
在 Java 开发中,性能优化是确保应用程序高效运行的关键。无论是构建实时处理系统还是大规模分布式服务,理解性能术语和分析方法都至关重要。本文将详细介绍 Java 性能中的核心术语,包括延迟(Latency)、吞吐量(Throughput)、利用率(Utilization)、效率(Efficiency)、…...
简单介绍Qt的属性子系统
深入理解Qt的属性系统 笔者最近正在大规模的开发Qt的项目和工程,这里笔者需要指出的是,这个玩意在最常规的Qt开发中是相对比较少用的,笔者也只是在Qt的QPropertyAnimation需要动画感知笔者设置的一个属性的时候方才知道这个东西的。因此&…...
【PmHub后端篇】PmHub中基于自定义注解和AOP的服务接口鉴权与内部认证实现
1 引言 在现代软件开发中,尤其是在微服务架构下,服务接口的鉴权和内部认证是保障系统安全的重要环节。本文将详细介绍PmHub中如何利用自定义注解和AOP(面向切面编程)实现服务接口的鉴权和内部认证,所涉及的技术知识点…...
消息~组件(群聊类型)ConcurrentHashMap发送
为什么选择ConcurrentHashMap? 在开发聊天应用时,我们需要存储和管理大量的聊天消息数据,这些数据会被多个线程频繁访问和修改。比如,当多个用户同时发送消息时,服务端需要同时处理这些消息的存储和查询。如果用普通的…...
掌控随心 - 服务网格的流量管理艺术 (Istio 实例)
掌控随心 - 服务网格的流量管理艺术 (Istio 实例) 想象一下,没有服务网格的时候,我们要实现像“将 1% 的用户流量导入到新版本应用”、“根据用户设备类型访问不同后端”、“模拟下游服务故障”这类高级流量策略,通常需要在代码、负载均衡器、API 网关等多个地方进行复杂且分…...
Github 2025-05-13 Python开源项目日报 Top10
根据Github Trendings的统计,今日(2025-05-13统计)共有10个项目上榜。根据开发语言中项目的数量,汇总情况如下: 开发语言项目数量Python项目10TypeScript项目1 ComfyUI:强大而模块化的稳定扩散GUI 创建周期:399 天开…...
Spring Boot 自动装配原理详解
Spring Boot 的自动装配(Auto-Configuration)是其核心特性之一,它极大地简化了 Spring 应用的配置过程。通过自动装配,Spring Boot 能够根据项目中的依赖(例如,添加了 Spring Data JPA 依赖后自动配置数据库…...
Python核心数据类型全解析:字符串、列表、元组、字典与集合
导读: Python 是一门功能强大且灵活的编程语言,而其核心数据类型是构建高效程序的基础。本文深入剖析了 Python 的五大核心数据类型——字符串、列表、元组、字典和集合,结合实际应用场景与最佳实践,帮助读者全面掌握这些数据类型…...
索尼(sony)摄像机格式化后mp4的恢复方法
索尼(sony)的Alpha 7 Ⅳ系列绝对称的上是索尼的“全画幅标杆机型”,A7M4配备了3300万像素的CMOS,以及全新研发的全画幅背照式Exmor R™CMOS影像传感器,搭载BIONZ XR™影像处理器,与旗舰微单™Alpha 1如出一辙。下面我们来看看A7M4…...
Kubernetes容器运行时:Containerd vs Docker
Containerd 和 Docker 是容器技术领域的两个核心组件,它们在功能定位、架构设计、性能特点及适用场景上有显著差异。以下是两者的详细对比分析: 一、定位与功能 特性DockerContainerd核心定位完整的容器平台,包含构建、运行、编排等全生命周…...
免费专业级 PDF 处理!SolidPDF OCR 识别 + 精准转换批量处理
各位办公小能手们!今天咱来聊聊一款超牛的软件——SolidConverterPDF。这可是个专业的多功能PDF处理工具,啥格式转换、文档编辑、扫描识别,它都能搞定!下面我就给大伙详细唠唠它的厉害之处。 先说说它的核心功能。 一是PDF格式转换…...
电子电器架构 --- 区域计算架构(Zonal Compute)备战下一代电子电气架构
我是穿拖鞋的汉子,魔都中坚持长期主义的汽车电子工程师。 老规矩,分享一段喜欢的文字,避免自己成为高知识低文化的工程师: 钝感力的“钝”,不是木讷、迟钝,而是直面困境的韧劲和耐力,是面对外界噪音的通透淡然。 生活中有两种人,一种人格外在意别人的眼光;另一种人无论…...
API的学习总结(上)
在 Java 中,API 指的是 Java 提供的一系列类、接口、方法和工具,用于开发 Java 应用程序。Java API 是 Java 平台的核心组成部分,它提供了丰富的功能,包括基础数据类型、集合框架、输入输出、网络编程、多线程、数据库连接等。 核…...
Spring Boot之Web服务器的启动流程分析
如何判断创建哪种web容器:servlet?reactive? 我们在启动Spring Boot程序的时候,会使用SpringApplication.run方法来启动,在启动流程中首先要判断的就是需要启动什么类型的服务器,是servlet?或者…...