当前位置: 首页 > news >正文

ffplay播放器剖析(5)----视频输出剖析

文章目录

  • 1.视频输出模块
    • 1.1 视频输出初始化
      • 1.1.1 视频输出初始化主要流程
      • 1.1.2 calculate_display_rect初始化显示窗口大小
    • 1.2 视频输出逻辑
      • 1.2.1 event_loop开始处理SDL事件
      • 1.2.2 video_refresh
        • 1.2.2.1 计算上一帧显示时长,判断是否还要继续上一帧
        • 1.2.2.2 估算当前帧显示时长,判断是否要丢帧
        • 1.2.2.3 调用video_display进行显示
        • 1.2.2.4 realloc_texture()重新分配vid_texture
        • 1.2.2.5 sws_getCachedContext
        • 1.2.2.6 sws_scale 图像转换
  • 对于flags算法测试推荐文章:

1.视频输出模块

1.1 视频输出初始化

1.1.1 视频输出初始化主要流程

  1. 初始化SDL,SDL_Init,主要是SDL_INIT_VIDEO的支持
  2. SDL_CreateWindow,创建主窗口
  3. SDL_CreateRender,基于主窗口创建renderer,用于渲染输出
  4. stream_open
  5. event_loop,播放控制事件的相应循环,但也负责了video的显示输出
int main(int argc, char **argv)
{/* 是否显示视频 */if (display_disable) {video_disable = 1;}// 3. SDL的初始化flags = SDL_INIT_VIDEO | SDL_INIT_AUDIO | SDL_INIT_TIMER;/* 是否运行音频 */if (audio_disable)flags &= ~SDL_INIT_AUDIO;else {/* Try to work around an occasional ALSA buffer underflow issue when the* period size is NPOT due to ALSA resampling by forcing the buffer size. */if (!SDL_getenv("SDL_AUDIO_ALSA_SET_BUFFER_SIZE"))SDL_setenv("SDL_AUDIO_ALSA_SET_BUFFER_SIZE","1", 1);}if (display_disable)flags &= ~SDL_INIT_VIDEO;if (SDL_Init (flags)) {av_log(NULL, AV_LOG_FATAL, "Could not initialize SDL - %s\n", SDL_GetError());av_log(NULL, AV_LOG_FATAL, "(Did you set the DISPLAY variable?)\n");exit(1);}SDL_EventState(SDL_SYSWMEVENT, SDL_IGNORE);SDL_EventState(SDL_USEREVENT, SDL_IGNORE);av_init_packet(&flush_pkt);				// 初始化flush_packetflush_pkt.data = (uint8_t *)&flush_pkt; // 初始化为数据指向自己本身// 4. 创建窗口if (!display_disable) {int flags = SDL_WINDOW_HIDDEN;if (alwaysontop)
#if SDL_VERSION_ATLEAST(2,0,5)flags |= SDL_WINDOW_ALWAYS_ON_TOP;
#elseav_log(NULL, AV_LOG_WARNING, "Your SDL version doesn't support SDL_WINDOW_ALWAYS_ON_TOP. Feature will be inactive.\n");
#endifif (borderless)flags |= SDL_WINDOW_BORDERLESS;elseflags |= SDL_WINDOW_RESIZABLE;window = SDL_CreateWindow(program_name, SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, default_width, default_height, flags);SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, "linear");if (window) {// 创建rendererrenderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC);if (!renderer) {av_log(NULL, AV_LOG_WARNING, "Failed to initialize a hardware accelerated renderer: %s\n", SDL_GetError());renderer = SDL_CreateRenderer(window, -1, 0);}if (renderer) {if (!SDL_GetRendererInfo(renderer, &renderer_info))av_log(NULL, AV_LOG_VERBOSE, "Initialized %s renderer.\n", renderer_info.name);}}if (!window || !renderer || !renderer_info.num_texture_formats) {av_log(NULL, AV_LOG_FATAL, "Failed to create window or renderer: %s", SDL_GetError());do_exit(NULL);}}// 5. 通过stream_open函数,开启read_thread读取线程is = stream_open(input_filename, file_iformat);if (!is) {av_log(NULL, AV_LOG_FATAL, "Failed to initialize VideoState!\n");do_exit(NULL);}// 6. 事件响应event_loop(is);/* never returns */return 0;
}
    //7 从待处理流中获取相关参数,设置显示窗口的宽度、高度及宽高比if (st_index[AVMEDIA_TYPE_VIDEO] >= 0) {AVStream *st = ic->streams[st_index[AVMEDIA_TYPE_VIDEO]];AVCodecParameters *codecpar = st->codecpar;//根据流和帧宽高比猜测视频帧的像素宽高比(像素的宽高比,注意不是图像的)AVRational sar = av_guess_sample_aspect_ratio(ic, st, NULL);if (codecpar->width) {// 设置显示窗口的大小和宽高比set_default_window_size(codecpar->width, codecpar->height, sar);}}

这里重点讲解一下set_default_window_size函数

static void set_default_window_size(int width, int height, AVRational sar)
{SDL_Rect rect;int max_width  = screen_width  ? screen_width  : INT_MAX; // 确定是否指定窗口最大宽度int max_height = screen_height ? screen_height : INT_MAX; // 确定是否指定窗口最大高度if (max_width == INT_MAX && max_height == INT_MAX)max_height = height;    // 没有指定最大高度时则使用视频的高度calculate_display_rect(&rect, 0, 0, max_width, max_height, width, height, sar);default_width  = rect.w; // 实际是渲染区域的宽高default_height = rect.h;
}

screen_width和screen_height可以在ffplay启动时用命令行进行设置 -x -y 如果没有指定那么就使用视频帧的高度

重点就是calculate_display_rect函数!

1.1.2 calculate_display_rect初始化显示窗口大小

static void calculate_display_rect(SDL_Rect *rect,int scr_xleft, int scr_ytop, int scr_width, int scr_height,int pic_width, int pic_height, AVRational pic_sar)
{AVRational aspect_ratio = pic_sar; // 比率int64_t width, height, x, y;if (av_cmp_q(aspect_ratio, av_make_q(0, 1)) <= 0)aspect_ratio = av_make_q(1, 1);// 如果aspect_ratio是负数或者为0,设置为1:1// 转成真正的播放比例aspect_ratio = av_mul_q(aspect_ratio, av_make_q(pic_width, pic_height));/* XXX: we suppose the screen has a 1.0 pixel ratio */// 计算显示视频帧区域的宽高// 先以高度为基准height = scr_height;// &~1, 取偶数宽度  1110width = av_rescale(height, aspect_ratio.num, aspect_ratio.den) & ~1;if (width > scr_width) {// 当以高度为基准,发现计算出来的需要的窗口宽度不足时调整为以窗口宽度为基准width = scr_width;height = av_rescale(width, aspect_ratio.den, aspect_ratio.num) & ~1;}// 计算显示视频帧区域的起始坐标(在显示窗口内部的区域)x = (scr_width - width) / 2;y = (scr_height - height) / 2;rect->x = scr_xleft + x;rect->y = scr_ytop  + y;rect->w = FFMAX((int)width,  1);rect->h = FFMAX((int)height, 1);
}

这个函数设置了计算了窗口的宽高和位置

函数是先计算宽高比,如果宽高比没有设置的话则使用实际宽高来计算宽高比,然后先以高度为基准使用av_rescale函数计算宽度,如果宽度大于scr_width就转变为以宽度为基准.

然后就是计算顶点坐标了,scr_width和scr_height窗口大小,width和heigth是视频大小,我们要计算出视频的左上角位置

看图:
在这里插入图片描述

代码中rect就是渲染的视频部分,也就是图中绿色部分!

1.2 视频输出逻辑

main()->event_loop()->refresh_loop_wait_event()->video_refresh()->video_display()->video_image_display()->upload_texture()

1.2.1 event_loop开始处理SDL事件

static void event_loop(VideoState *cur_stream)
{SDL_Event event;double incr, pos, frac;for (;;) {double x;refresh_loop_wait_event(cur_stream, &event); //video是在这里显示的switch (event.type) {case SDL_KEYDOWN:	/* 键盘事件 */if (exit_on_keydown || event.key.keysym.sym == SDLK_ESCAPE || event.key.keysym.sym == SDLK_q) {do_exit(cur_stream);break;}if (!cur_stream->width)continue;switch (event.key.keysym.sym) {case SDLK_f:toggle_full_screen(cur_stream);cur_stream->force_refresh = 1;break;case SDLK_p:case SDLK_SPACE: //按空格键触发暂停/恢复toggle_pause(cur_stream);break;case SDLK_m:toggle_mute(cur_stream);break;case SDLK_KP_MULTIPLY:case SDLK_0:update_volume(cur_stream, 1, SDL_VOLUME_STEP);break;case SDLK_KP_DIVIDE:case SDLK_9:update_volume(cur_stream, -1, SDL_VOLUME_STEP);break;case SDLK_s: // S: Step to next framestep_to_next_frame(cur_stream);break;case SDLK_a:stream_cycle_channel(cur_stream, AVMEDIA_TYPE_AUDIO);break;case SDLK_v:stream_cycle_channel(cur_stream, AVMEDIA_TYPE_VIDEO);break;case SDLK_c:stream_cycle_channel(cur_stream, AVMEDIA_TYPE_VIDEO);stream_cycle_channel(cur_stream, AVMEDIA_TYPE_AUDIO);stream_cycle_channel(cur_stream, AVMEDIA_TYPE_SUBTITLE);break;case SDLK_t:stream_cycle_channel(cur_stream, AVMEDIA_TYPE_SUBTITLE);break;case SDLK_w:
#if CONFIG_AVFILTERif (cur_stream->show_mode == SHOW_MODE_VIDEO && cur_stream->vfilter_idx < nb_vfilters - 1) {if (++cur_stream->vfilter_idx >= nb_vfilters)cur_stream->vfilter_idx = 0;} else {cur_stream->vfilter_idx = 0;toggle_audio_display(cur_stream);}
#elsetoggle_audio_display(cur_stream);
#endifbreak;case SDLK_PAGEUP:if (cur_stream->ic->nb_chapters <= 1) {incr = 600.0;goto do_seek;}seek_chapter(cur_stream, 1);break;case SDLK_PAGEDOWN:if (cur_stream->ic->nb_chapters <= 1) {incr = -600.0;goto do_seek;}seek_chapter(cur_stream, -1);break;case SDLK_LEFT:incr = seek_interval ? -seek_interval : -10.0;goto do_seek;case SDLK_RIGHT:incr = seek_interval ? seek_interval : 10.0;goto do_seek;case SDLK_UP:incr = 60.0;goto do_seek;case SDLK_DOWN:incr = -60.0;do_seek:if (seek_by_bytes) {pos = -1;if (pos < 0 && cur_stream->video_stream >= 0)pos = frame_queue_last_pos(&cur_stream->pictq);if (pos < 0 && cur_stream->audio_stream >= 0)pos = frame_queue_last_pos(&cur_stream->sampq);if (pos < 0)pos = avio_tell(cur_stream->ic->pb);if (cur_stream->ic->bit_rate)incr *= cur_stream->ic->bit_rate / 8.0;elseincr *= 180000.0;pos += incr;stream_seek(cur_stream, pos, incr, 1);} else {pos = get_master_clock(cur_stream);if (isnan(pos))pos = (double)cur_stream->seek_pos / AV_TIME_BASE;pos += incr;    // 现在是秒的单位if (cur_stream->ic->start_time != AV_NOPTS_VALUE && pos < cur_stream->ic->start_time / (double)AV_TIME_BASE)pos = cur_stream->ic->start_time / (double)AV_TIME_BASE;stream_seek(cur_stream, (int64_t)(pos * AV_TIME_BASE), (int64_t)(incr * AV_TIME_BASE), 0);}break;default:break;}break;case SDL_MOUSEBUTTONDOWN:			/* 鼠标按下事件 */if (exit_on_mousedown) {do_exit(cur_stream);break;}if (event.button.button == SDL_BUTTON_LEFT) {static int64_t last_mouse_left_click = 0;if (av_gettime_relative() - last_mouse_left_click <= 500000) {//连续鼠标左键点击2次显示窗口间隔小于0.5秒,则进行全屏或者恢复原始窗口toggle_full_screen(cur_stream);cur_stream->force_refresh = 1;last_mouse_left_click = 0;} else {last_mouse_left_click = av_gettime_relative();}}case SDL_MOUSEMOTION:		/* 鼠标移动事件 */if (cursor_hidden) {SDL_ShowCursor(1);cursor_hidden = 0;}cursor_last_shown = av_gettime_relative();if (event.type == SDL_MOUSEBUTTONDOWN) {if (event.button.button != SDL_BUTTON_RIGHT)break;x = event.button.x;} else {if (!(event.motion.state & SDL_BUTTON_RMASK))break;x = event.motion.x;}if (seek_by_bytes || cur_stream->ic->duration <= 0) {uint64_t size =  avio_size(cur_stream->ic->pb); // 整个文件的字节stream_seek(cur_stream, size*x/cur_stream->width, 0, 1);} else {int64_t ts;int ns, hh, mm, ss;int tns, thh, tmm, tss;tns  = cur_stream->ic->duration / 1000000LL;thh  = tns / 3600;tmm  = (tns % 3600) / 60;tss  = (tns % 60);frac = x / cur_stream->width;ns   = frac * tns;hh   = ns / 3600;mm   = (ns % 3600) / 60;ss   = (ns % 60);av_log(NULL, AV_LOG_INFO,"Seek to %2.0f%% (%2d:%02d:%02d) of total duration (%2d:%02d:%02d)       \n", frac*100,hh, mm, ss, thh, tmm, tss);ts = frac * cur_stream->ic->duration;if (cur_stream->ic->start_time != AV_NOPTS_VALUE)ts += cur_stream->ic->start_time;stream_seek(cur_stream, ts, 0, 0);}break;case SDL_WINDOWEVENT:		/* 窗口事件 */switch (event.window.event) {case SDL_WINDOWEVENT_SIZE_CHANGED:screen_width  = cur_stream->width  = event.window.data1;screen_height = cur_stream->height = event.window.data2;if (cur_stream->vis_texture) {SDL_DestroyTexture(cur_stream->vis_texture);cur_stream->vis_texture = NULL;}case SDL_WINDOWEVENT_EXPOSED:cur_stream->force_refresh = 1;}break;case SDL_QUIT:case FF_QUIT_EVENT:	/* ffplay自定义事件,用于主动退出 */do_exit(cur_stream);break;default:break;}}
}

这个函数主要是通过refresh_loop_wait_event函数等待事件,然后event_loop进行处理事件.

video的显示主要在refresh_loop_wait_event中:

static void refresh_loop_wait_event(VideoState *is, SDL_Event *event) {double remaining_time = 0.0; /* 休眠等待,remaining_time的计算在video_refresh中 *//* 调用SDL_PeepEvents前先调用SDL_PumpEvents,将输入设备的事件抽到事件队列中 */SDL_PumpEvents();/** SDL_PeepEvents check是否事件,比如鼠标移入显示区等* 从事件队列中拿一个事件,放到event中,如果没有事件,则进入循环中* SDL_PeekEvents用于读取事件,在调用该函数之前,必须调用SDL_PumpEvents搜集键盘等事件*/while (!SDL_PeepEvents(event, 1, SDL_GETEVENT, SDL_FIRSTEVENT, SDL_LASTEVENT)) {if (!cursor_hidden && av_gettime_relative() - cursor_last_shown > CURSOR_HIDE_DELAY) {SDL_ShowCursor(0);cursor_hidden = 1;}/** remaining_time就是用来进行音视频同步的。* 在video_refresh函数中,根据当前帧显示时刻(display time)和实际时刻(actual time)* 计算需要sleep的时间,保证帧按时显示*/if (remaining_time > 0.0)   //sleep控制画面输出的时机av_usleep((int64_t)(remaining_time * 1000000.0)); // remaining_time <= REFRESH_RATEremaining_time = REFRESH_RATE;if (is->show_mode != SHOW_MODE_NONE && // 显示模式不等于SHOW_MODE_NONE(!is->paused  // 非暂停状态|| is->force_refresh) // 强制刷新状态) {video_refresh(is, &remaining_time);}/* 从输入设备中搜集事件,推动这些事件进入事件队列,更新事件队列的状态,* 不过它还有一个作用是进行视频子系统的设备状态更新,如果不调用这个函数,* 所显示的视频会在大约10秒后丢失色彩。没有调用SDL_PumpEvents,将不会* 有任何的输入设备事件进入队列,这种情况下,SDL就无法响应任何的键盘等硬件输入。*/SDL_PumpEvents();}
}

SDL_PeepEvent通过SDL_GETEVENT非阻塞查询队列中是否有事件,如果不为0则有事件发生(-1表示发生错误),那么函数就会返回,让event_loop进行处理;否则调用video_refresh进行显示画面,并且通过输出参数remaining_time获取下一轮应当sleep的时间以保证画面稳定输出.

是否调用video_refresh的前置条件为:

  1. 显示模式不为SHOW_MODE_NONE(如果只包含audio的画,也可能为其波形图)
  2. 当前没有被暂停
  3. 当设置了force_refresh(强制刷新),分析一下出现的情况:
    • video_refresh里面帧显示,常规情况.
    • SDL_WINDOWEVENT_EXPOSED,窗口需要重新绘制
    • SDL_MOUSEBUTTONDOWN && SDL_BUTTON_LEFT 鼠标按下并且按左键连续间隔小于0.5s
    • SDLK_f,按f键进行全屏或者恢复原始播放窗口

有可能理解不了这个强制刷新是什么,看一下没有强制刷新的效果图:

在这里插入图片描述

可见下面一部分残缺了,因为我们改变了窗口大小,但是我们渲染的页面没有改变,这时我们重新刷新一下可以放渲染页面跟着窗口大小进行改变

1.2.2 video_refresh

static void video_refresh(void *opaque, double *remaining_time)
{VideoState *is = opaque;double time;Frame *sp, *sp2;if (!is->paused && get_master_sync_type(is) == AV_SYNC_EXTERNAL_CLOCK && is->realtime)check_external_clock_speed(is);if (!display_disable && is->show_mode != SHOW_MODE_VIDEO && is->audio_st) {time = av_gettime_relative() / 1000000.0;if (is->force_refresh || is->last_vis_time + rdftspeed < time) {video_display(is);is->last_vis_time = time;}*remaining_time = FFMIN(*remaining_time, is->last_vis_time + rdftspeed - time);}if (is->video_st) {retry:if (frame_queue_nb_remaining(&is->pictq) == 0) {// 帧队列是否为空// nothing to do, no picture to display in the queue// 什么都不做,队列中没有图像可显示} else { // 重点是音视频同步double last_duration, duration, delay;Frame *vp, *lastvp;/* dequeue the picture */// 从队列取出上一个Framelastvp = frame_queue_peek_last(&is->pictq);//读取上一帧vp = frame_queue_peek(&is->pictq);  // 读取待显示帧// lastvp 上一帧(正在显示的帧)// vp 等待显示的帧if (vp->serial != is->videoq.serial) {// 如果不是最新的播放序列,则将其出队列,以尽快读取最新序列的帧frame_queue_next(&is->pictq);goto retry;}if (lastvp->serial != vp->serial) {// 新的播放序列重置当前时间is->frame_timer = av_gettime_relative() / 1000000.0;}if (is->paused){goto display;printf("视频暂停is->paused");}/* compute nominal last_duration *///lastvp上一帧,vp当前帧 ,nextvp下一帧//last_duration 计算上一帧应显示的时长last_duration = vp_duration(is, lastvp, vp);// 经过compute_target_delay方法,计算出待显示帧vp需要等待的时间// 如果以video同步,则delay直接等于last_duration。// 如果以audio或外部时钟同步,则需要比对主时钟调整待显示帧vp要等待的时间。delay = compute_target_delay(last_duration, is); // 上一帧需要维持的时间time= av_gettime_relative()/1000000.0;// is->frame_timer 实际上就是上一帧lastvp的播放时间,// is->frame_timer + delay 是待显示帧vp该播放的时间if (time < is->frame_timer + delay) { //判断是否继续显示上一帧// 当前系统时刻还未到达上一帧的结束时刻,那么还应该继续显示上一帧。// 计算出最小等待时间*remaining_time = FFMIN(is->frame_timer + delay - time, *remaining_time);goto display;}// 走到这一步,说明已经到了或过了该显示的时间,待显示帧vp的状态变更为当前要显示的帧is->frame_timer += delay;   // 更新当前帧播放的时间if (delay > 0 && time - is->frame_timer > AV_SYNC_THRESHOLD_MAX) {is->frame_timer = time; //如果和系统时间差距太大,就纠正为系统时间}SDL_LockMutex(is->pictq.mutex);if (!isnan(vp->pts))update_video_pts(is, vp->pts, vp->pos, vp->serial); // 更新video时钟SDL_UnlockMutex(is->pictq.mutex);//丢帧逻辑if (frame_queue_nb_remaining(&is->pictq) > 1) {//有nextvp才会检测是否该丢帧Frame *nextvp = frame_queue_peek_next(&is->pictq);duration = vp_duration(is, vp, nextvp);if(!is->step        // 非逐帧模式才检测是否需要丢帧 is->step==1 为逐帧播放&& (framedrop>0 ||      // cpu解帧过慢(framedrop && get_master_sync_type(is) != AV_SYNC_VIDEO_MASTER)) // 非视频同步方式&& time > is->frame_timer + duration // 确实落后了一帧数据) {printf("%s(%d) dif:%lfs, drop frame\n", __FUNCTION__, __LINE__,(is->frame_timer + duration) - time);is->frame_drops_late++;             // 统计丢帧情况frame_queue_next(&is->pictq);       // 这里实现真正的丢帧//(这里不能直接while丢帧,因为很可能audio clock重新对时了,这样delay值需要重新计算)goto retry; //回到函数开始位置,继续重试}}if (is->subtitle_st) {while (frame_queue_nb_remaining(&is->subpq) > 0) {sp = frame_queue_peek(&is->subpq);if (frame_queue_nb_remaining(&is->subpq) > 1)sp2 = frame_queue_peek_next(&is->subpq);elsesp2 = NULL;if (sp->serial != is->subtitleq.serial|| (is->vidclk.pts > (sp->pts + ((float) sp->sub.end_display_time / 1000)))|| (sp2 && is->vidclk.pts > (sp2->pts + ((float) sp2->sub.start_display_time / 1000)))){if (sp->uploaded) {int i;for (i = 0; i < sp->sub.num_rects; i++) {AVSubtitleRect *sub_rect = sp->sub.rects[i];uint8_t *pixels;int pitch, j;if (!SDL_LockTexture(is->sub_texture, (SDL_Rect *)sub_rect, (void **)&pixels, &pitch)) {for (j = 0; j < sub_rect->h; j++, pixels += pitch)memset(pixels, 0, sub_rect->w << 2);SDL_UnlockTexture(is->sub_texture);}}}frame_queue_next(&is->subpq);} else {break;}}}frame_queue_next(&is->pictq);   // 当前vp帧出队列is->force_refresh = 1;          /* 说明需要刷新视频帧 */if (is->step && !is->paused)stream_toggle_pause(is);    // 逐帧的时候那继续进入暂停状态}display:/* display picture */if (!display_disable && is->force_refresh && is->show_mode == SHOW_MODE_VIDEO && is->pictq.rindex_shown)video_display(is); // 重点是显示}is->force_refresh = 0;if (show_status) {static int64_t last_time;int64_t cur_time;int aqsize, vqsize, sqsize;double av_diff;cur_time = av_gettime_relative();if (!last_time || (cur_time - last_time) >= 30000) {aqsize = 0;vqsize = 0;sqsize = 0;if (is->audio_st)aqsize = is->audioq.size;if (is->video_st)vqsize = is->videoq.size;if (is->subtitle_st)sqsize = is->subtitleq.size;av_diff = 0;if (is->audio_st && is->video_st)av_diff = get_clock(&is->audclk) - get_clock(&is->vidclk);else if (is->video_st)av_diff = get_master_clock(is) - get_clock(&is->vidclk);else if (is->audio_st)av_diff = get_master_clock(is) - get_clock(&is->audclk);av_log(NULL, AV_LOG_INFO,"%7.2f %s:%7.3f fd=%4d aq=%5dKB vq=%5dKB sq=%5dB f=%"PRId64"/%"PRId64"   \r",get_master_clock(is),(is->audio_st && is->video_st) ? "A-V" : (is->video_st ? "M-V" : (is->audio_st ? "M-A" : "   ")),av_diff,is->frame_drops_early + is->frame_drops_late,aqsize / 1024,vqsize / 1024,sqsize,is->video_st ? is->viddec.avctx->pts_correction_num_faulty_dts : 0,is->video_st ? is->viddec.avctx->pts_correction_num_faulty_pts : 0);fflush(stdout);last_time = cur_time;}}
}

流程图:

看主流程图:

  1. 取出上一帧和待显示的帧
  2. 计算上一帧显示的时长,判断当前是否继续上一帧
  3. 估算当前帧显示时长,判断是否要丢帧
  4. 调用video_display显示

1.2.2.1 计算上一帧显示时长,判断是否还要继续上一帧

首先判断pictq是否为空(调用frame_queue_nb_remaining判断是否还有未显示的帧),如果为空则继续调用video_display显示上一帧

在进一步计算上一帧显示时间之前,需要先判断下一帧vp是否为最新序列,也就是说if(vp->serial!= is->videoq.serial),如果条件成立就是发生过seek等操作,此时应该丢弃lastvp. 故调用frame_queue_next抛弃lastvp后,返回流程开头重试.

接下来 可以计算lastvp显示时长了. 计算代码为:

last_duration = vp_duration(is, lastvp, vp);
delay = compute_target_delay(last_duration, is);//返回当前显示帧要持续播放的时间

本质就是通过上一帧和待显示帧pts来计算的,如果考虑到同步,则还需要考虑当前与主时钟的差距来决定是重复上一帧,还是丢帧,还是正常显示下一帧(待显示帧)

            time= av_gettime_relative()/1000000.0;// is->frame_timer 实际上就是上一帧lastvp的播放时间,// is->frame_timer + delay 是待显示帧vp该播放的时间if (time < is->frame_timer + delay) { //判断是否继续显示上一帧// 当前系统时刻还未到达上一帧的结束时刻,那么还应该继续显示上一帧。// 计算出最小等待时间*remaining_time = FFMIN(is->frame_timer + delay - time, *remaining_time);goto display;}

frame_timer就是当前帧显示时间,如果当前帧显示时间+delay显示时间大于当前系统时间的话就继续显示上一帧!

在这里插入图片描述

1.2.2.2 估算当前帧显示时长,判断是否要丢帧

  is->frame_timer += delay;   // 更新当前帧播放的时间if (delay > 0 && time - is->frame_timer > AV_SYNC_THRESHOLD_MAX) {is->frame_timer = time; //如果和系统时间差距太大,就纠正为系统时间}

判断是否需要丢帧

 if (frame_queue_nb_remaining(&is->pictq) > 1) {//有nextvp才会检测是否该丢帧Frame *nextvp = frame_queue_peek_next(&is->pictq);duration = vp_duration(is, vp, nextvp);if(!is->step        // 非逐帧模式才检测是否需要丢帧 is->step==1 为逐帧播放&& (framedrop>0 ||      // cpu解帧过慢(framedrop && get_master_sync_type(is) != AV_SYNC_VIDEO_MASTER)) // 非视频同步方式&& time > is->frame_timer + duration // 确实落后了一帧数据) {printf("%s(%d) dif:%lfs, drop frame\n", __FUNCTION__, __LINE__,(is->frame_timer + duration) - time);is->frame_drops_late++;             // 统计丢帧情况frame_queue_next(&is->pictq);       // 这里实现真正的丢帧//(这里不能直接while丢帧,因为很可能audio clock重新对时了,这样delay值需要重新计算)goto retry; //回到函数开始位置,继续重试}}

通过待显示帧和下一帧来计算当前帧显示时间(前提是得有下一帧)

并且要符合以下条件才会丢帧:

  1. 不处于step状态,逐帧状态
  2. 启动framedrop模式,也就是cpu过慢时需要丢帧,并且不是以video为同步时钟的情况下
  3. 当前时间已经>frame_timer+duration

1.2.2.3 调用video_display进行显示

static void video_display(VideoState *is)
{if (!is->width)video_open(is); //如果窗口未显示,则显示窗口SDL_SetRenderDrawColor(renderer, 0, 0, 0, 255);SDL_RenderClear(renderer);if (is->audio_st && is->show_mode != SHOW_MODE_VIDEO)video_audio_display(is);    //图形化显示仅有音轨的文件else if (is->video_st)video_image_display(is);    //显示一帧视频画面SDL_RenderPresent(renderer);
}
static void video_image_display(VideoState *is)
{Frame *vp;Frame *sp = NULL;SDL_Rect rect;// keep_last的作用就出来了,我们是有调用frame_queue_next, 但最近出队列的帧并没有真正销毁// 所以这里可以读取出来显示vp = frame_queue_peek_last(&is->pictq); //if (is->subtitle_st) {if (frame_queue_nb_remaining(&is->subpq) > 0) {sp = frame_queue_peek(&is->subpq);if (vp->pts >= sp->pts + ((float) sp->sub.start_display_time / 1000)) {if (!sp->uploaded) {uint8_t* pixels[4];int pitch[4];int i;if (!sp->width || !sp->height) {sp->width = vp->width;sp->height = vp->height;}if (realloc_texture(&is->sub_texture, SDL_PIXELFORMAT_ARGB8888, sp->width, sp->height, SDL_BLENDMODE_BLEND, 1) < 0)return;for (i = 0; i < sp->sub.num_rects; i++) {AVSubtitleRect *sub_rect = sp->sub.rects[i];sub_rect->x = av_clip(sub_rect->x, 0, sp->width );sub_rect->y = av_clip(sub_rect->y, 0, sp->height);sub_rect->w = av_clip(sub_rect->w, 0, sp->width  - sub_rect->x);sub_rect->h = av_clip(sub_rect->h, 0, sp->height - sub_rect->y);is->sub_convert_ctx = sws_getCachedContext(is->sub_convert_ctx,sub_rect->w, sub_rect->h, AV_PIX_FMT_PAL8,sub_rect->w, sub_rect->h, AV_PIX_FMT_BGRA,0, NULL, NULL, NULL);if (!is->sub_convert_ctx) {av_log(NULL, AV_LOG_FATAL, "Cannot initialize the conversion context\n");return;}if (!SDL_LockTexture(is->sub_texture, (SDL_Rect *)sub_rect, (void **)pixels, pitch)) {sws_scale(is->sub_convert_ctx, (const uint8_t * const *)sub_rect->data, sub_rect->linesize,0, sub_rect->h, pixels, pitch);SDL_UnlockTexture(is->sub_texture);}}sp->uploaded = 1;}} elsesp = NULL;}}//将帧宽高按照sar最大适配到窗口,并通过rect返回视频帧在窗口的显示位置和宽高calculate_display_rect(&rect, is->xleft, is->ytop, is->width, is->height,vp->width, vp->height, vp->sar);//    rect.x = rect.w /2;   // 测试//    rect.w = rect.w /2;   // 缩放实际不是用sws, 缩放是sdl去做的if (!vp->uploaded) {// 把yuv数据更新到vid_textureif (upload_texture(&is->vid_texture, vp->frame, &is->img_convert_ctx) < 0)return;vp->uploaded = 1;vp->flip_v = vp->frame->linesize[0] < 0;}set_sdl_yuv_conversion_mode(vp->frame);SDL_RenderCopyEx(renderer, is->vid_texture, NULL, &rect, 0, NULL, vp->flip_v ? SDL_FLIP_VERTICAL : 0);set_sdl_yuv_conversion_mode(NULL);if (sp) {
#if USE_ONEPASS_SUBTITLE_RENDERSDL_RenderCopy(renderer, is->sub_texture, NULL, &rect);
#elseint i;double xratio = (double)rect.w / (double)sp->width;double yratio = (double)rect.h / (double)sp->height;for (i = 0; i < sp->sub.num_rects; i++) {SDL_Rect *sub_rect = (SDL_Rect*)sp->sub.rects[i];SDL_Rect target = {.x = rect.x + sub_rect->x * xratio,.y = rect.y + sub_rect->y * yratio,.w = sub_rect->w * xratio,.h = sub_rect->h * yratio};SDL_RenderCopy(renderer, is->sub_texture, sub_rect, &target);}
#endif}
}

video_image_display整体不算复杂,每次渲染都会调用calculate_display_rect进行重新计算显示窗口等

最主要的显示是调用upload_texture将AVFrame的图像数据传给sdl的纹理进行渲染:

static int upload_texture(SDL_Texture **tex, AVFrame *frame, struct SwsContext **img_convert_ctx) {int ret = 0;Uint32 sdl_pix_fmt;SDL_BlendMode sdl_blendmode;// 根据frame中的图像格式(FFmpeg像素格式),获取对应的SDL像素格式和blendmodeget_sdl_pix_fmt_and_blendmode(frame->format, &sdl_pix_fmt, &sdl_blendmode);// 参数tex实际是&is->vid_texture,此处根据得到的SDL像素格式,为&is->vid_textureif (realloc_texture(tex, sdl_pix_fmt == SDL_PIXELFORMAT_UNKNOWN ? SDL_PIXELFORMAT_ARGB8888 : sdl_pix_fmt,frame->width, frame->height, sdl_blendmode, 0) < 0)return -1;//根据sdl_pix_fmt从AVFrame中取数据填充纹理switch (sdl_pix_fmt) {// frame格式是SDL不支持的格式,则需要进行图像格式转换,转换为目标格式AV_PIX_FMT_BGRA,// 对应SDL_PIXELFORMAT_BGRA32case SDL_PIXELFORMAT_UNKNOWN:/* This should only happen if we are not using avfilter... */*img_convert_ctx = sws_getCachedContext(*img_convert_ctx,frame->width, frame->height, frame->format,frame->width, frame->height, AV_PIX_FMT_BGRA,sws_flags, NULL, NULL, NULL);if (*img_convert_ctx != NULL) {uint8_t *pixels[4]; // 之前取Texture的缓存int pitch[4];if (!SDL_LockTexture(*tex, NULL, (void **)pixels, pitch)) {sws_scale(*img_convert_ctx, (const uint8_t * const *)frame->data, frame->linesize,0, frame->height, pixels, pitch);SDL_UnlockTexture(*tex);}} else {av_log(NULL, AV_LOG_FATAL, "Cannot initialize the conversion context\n");ret = -1;}break;// frame格式对应SDL_PIXELFORMAT_IYUV,不用进行图像格式转换,调用SDL_UpdateYUVTexture()更新SDL texturecase SDL_PIXELFORMAT_IYUV:if (frame->linesize[0] > 0 && frame->linesize[1] > 0 && frame->linesize[2] > 0) {ret = SDL_UpdateYUVTexture(*tex, NULL, frame->data[0], frame->linesize[0],frame->data[1], frame->linesize[1],frame->data[2], frame->linesize[2]);} else if (frame->linesize[0] < 0 && frame->linesize[1] < 0 && frame->linesize[2] < 0) {ret = SDL_UpdateYUVTexture(*tex, NULL, frame->data[0] + frame->linesize[0] * (frame->height                    - 1), -frame->linesize[0],frame->data[1] + frame->linesize[1] * (AV_CEIL_RSHIFT(frame->height, 1) - 1), -frame->linesize[1],frame->data[2] + frame->linesize[2] * (AV_CEIL_RSHIFT(frame->height, 1) - 1), -frame->linesize[2]);} else {av_log(NULL, AV_LOG_ERROR, "Mixed negative and positive linesizes are not supported.\n");return -1;}break;// frame格式对应其他SDL像素格式,不用进行图像格式转换,调用SDL_UpdateTexture()更新SDL texturedefault:if (frame->linesize[0] < 0) {ret = SDL_UpdateTexture(*tex, NULL, frame->data[0] + frame->linesize[0] * (frame->height - 1), -frame->linesize[0]);} else {ret = SDL_UpdateTexture(*tex, NULL, frame->data[0], frame->linesize[0]);}break;}return ret;
}

frame中的像素格式是FFmpeg中定义的像素格式,FFmpeg中定义的很多像素格式与SDL中定义的像素格式是同一种格式,只不过是名称不同

根据frame中的像素格式与SDL的像素格式的匹配情况,upload_texture()处理三种类型,对应的是Switch语句的三种分支:

  1. 如果frame图像格式对应SDL_PIXELFORMAT_IYUV格式,则不需要图像格式转换,使用SDL_updateYUVBTexture()将数据更新到&is->vid_texture中
  2. 如果frame图像格式对应SDL其他支持的格式,也不需要进行图像格式转换,使用SDL_updateTexture()将数据更新到&is->vid_texture中
  3. 如果frame图像不被SDL支持的话,则需要进行图像格式转换

根据映射表获取frame对应SDL中的像素格式:

static void get_sdl_pix_fmt_and_blendmode(int format, Uint32 *sdl_pix_fmt, SDL_BlendMode *sdl_blendmode)
{int i;*sdl_blendmode = SDL_BLENDMODE_NONE;*sdl_pix_fmt = SDL_PIXELFORMAT_UNKNOWN;if (format == AV_PIX_FMT_RGB32   ||format == AV_PIX_FMT_RGB32_1 ||format == AV_PIX_FMT_BGR32   ||format == AV_PIX_FMT_BGR32_1)*sdl_blendmode = SDL_BLENDMODE_BLEND;for (i = 0; i < FF_ARRAY_ELEMS(sdl_texture_format_map) - 1; i++) {if (format == sdl_texture_format_map[i].format) {*sdl_pix_fmt = sdl_texture_format_map[i].texture_fmt;return;}}
}

映射表:

    static const struct TextureFormatEntry {enum AVPixelFormat format;int texture_fmt;}
sdl_texture_format_map[] = {  // FFmpeg PIX_FMT to SDL_PIX的映射关系{ AV_PIX_FMT_RGB8,           SDL_PIXELFORMAT_RGB332 },{ AV_PIX_FMT_RGB444,         SDL_PIXELFORMAT_RGB444 },{ AV_PIX_FMT_RGB555,         SDL_PIXELFORMAT_RGB555 },{ AV_PIX_FMT_BGR555,         SDL_PIXELFORMAT_BGR555 },{ AV_PIX_FMT_RGB565,         SDL_PIXELFORMAT_RGB565 },{ AV_PIX_FMT_BGR565,         SDL_PIXELFORMAT_BGR565 },{ AV_PIX_FMT_RGB24,          SDL_PIXELFORMAT_RGB24 },{ AV_PIX_FMT_BGR24,          SDL_PIXELFORMAT_BGR24 },{ AV_PIX_FMT_0RGB32,         SDL_PIXELFORMAT_RGB888 },{ AV_PIX_FMT_0BGR32,         SDL_PIXELFORMAT_BGR888 },{ AV_PIX_FMT_NE(RGB0, 0BGR), SDL_PIXELFORMAT_RGBX8888 },{ AV_PIX_FMT_NE(BGR0, 0RGB), SDL_PIXELFORMAT_BGRX8888 },{ AV_PIX_FMT_RGB32,          SDL_PIXELFORMAT_ARGB8888 },{ AV_PIX_FMT_RGB32_1,        SDL_PIXELFORMAT_RGBA8888 },{ AV_PIX_FMT_BGR32,          SDL_PIXELFORMAT_ABGR8888 },{ AV_PIX_FMT_BGR32_1,        SDL_PIXELFORMAT_BGRA8888 },{ AV_PIX_FMT_YUV420P,        SDL_PIXELFORMAT_IYUV },{ AV_PIX_FMT_YUYV422,        SDL_PIXELFORMAT_YUY2 },{ AV_PIX_FMT_UYVY422,        SDL_PIXELFORMAT_UYVY },{ AV_PIX_FMT_NONE,           SDL_PIXELFORMAT_UNKNOWN },};

可以看到,除了最后⼀项,其他格式的图像送给SDL是可以直接显示的,不必进行图像转换。

1.2.2.4 realloc_texture()重新分配vid_texture

static int realloc_texture(SDL_Texture **texture, Uint32 new_format, int new_width, int new_height,SDL_BlendMode blendmode, int init_texture)
{Uint32 format;int access, w, h;if (!*texture || SDL_QueryTexture(*texture, &format, &access, &w, &h) < 0 || new_width != w || new_height != h || new_format != format) {void *pixels;int pitch;if (*texture)SDL_DestroyTexture(*texture);if (!(*texture = SDL_CreateTexture(renderer, new_format, SDL_TEXTUREACCESS_STREAMING, new_width, new_height)))return -1;if (SDL_SetTextureBlendMode(*texture, blendmode) < 0)return -1;if (init_texture) {if (SDL_LockTexture(*texture, NULL, &pixels, &pitch) < 0)return -1;memset(pixels, 0, pitch * new_height);SDL_UnlockTexture(*texture);}av_log(NULL, AV_LOG_VERBOSE, "Created %dx%d texture with %s.\n", new_width, new_height, SDL_GetPixelFormatName(new_format));}return 0;
}

什么情况下需要realloc_texture?

  1. 用于显示的texture没有分配
  2. SDL_QueryTexture无效
  3. 目前texture的width,height,format和新药显示的Frame不一致

综上所述:窗口大小变化不足以让realloc_texture重新SDL_CreateTextue

1.2.2.5 sws_getCachedContext

struct SwsContext *sws_getCachedContext(struct SwsContext *context,int srcW, int srcH, enum AVPixelFormat srcFormat,int dstW, int dstH, enum AVPixelFormat dstFormat,int flags, SwsFilter *srcFilter,SwsFilter *dstFilter, const double *param);

创建一个图像转换的上下文

这里需要说明的flags这个参数,这个参数是选择转换算法的,有很多转换算法,在libswscale/swscale.h文件中

1.2.2.6 sws_scale 图像转换

int sws_scale(struct SwsContext *c, const uint8_t *const srcSlice[],const int srcStride[], int srcSliceY, int srcSliceH,uint8_t *const dst[], const int dstStride[]);
            if (!SDL_LockTexture(*tex, NULL, (void **)pixels, pitch)) {sws_scale(*img_convert_ctx, (const uint8_t * const *)frame->data, frame->linesize,0, frame->height, pixels, pitch);SDL_UnlockTexture(*tex);}

对于flags算法测试推荐文章:

(66条消息) ffmpeg中的sws_scale算法性能测试_ffmpeg算法_雷霄骅的博客-CSDN博客

相关文章:

ffplay播放器剖析(5)----视频输出剖析

文章目录 1.视频输出模块1.1 视频输出初始化1.1.1 视频输出初始化主要流程1.1.2 calculate_display_rect初始化显示窗口大小 1.2 视频输出逻辑1.2.1 event_loop开始处理SDL事件1.2.2 video_refresh1.2.2.1 计算上一帧显示时长,判断是否还要继续上一帧1.2.2.2 估算当前帧显示时长…...

21.2:象棋走马问题

请同学们自行搜索或者想象一个象棋的棋盘&#xff0c; 然后把整个棋盘放入第一象限&#xff0c;棋盘的最左下角是(0,0)位置 那么整个棋盘就是横坐标上9条线、纵坐标上10条线的区域 给你三个 参数 x&#xff0c;y&#xff0c;k 返回“马”从(0,0)位置出发&#xff0c;必须走k步 …...

【CSS】手写 Tooltip 提示组件

文章目录 效果示例代码实现 效果示例 代码实现 <!DOCTYPE html> <html><head><meta charset"utf-8"><title>一颗不甘坠落的流星</title><style>body {padding: 120px;}.tooltip {position: relative;display: inline-blo…...

MySQL DDL语法

MySQL DDL语法 DDL简介 MySQL DDL&#xff08;Data Definition Language&#xff09;是用于定义和管理数据库结构的语言。它包括创建、修改和删除数据库、表、视图、索引和其他数据库对象的语句。DDL语法的重要性如下&#xff1a; 数据库结构定义&#xff1a;DDL语句用于创建…...

Git 绑定账号 和clone

一:环境: 下载安装完成Git,在桌面或文件夹下(在你将要保存代码的位置)右击可以看到Git Bash Here,点击可以进入黑窗口 二:配置公钥 1.查看当前状态(如果已绑定,且知道密码可以登陆,可以直接获取SSH公钥并配置即可拉取代码) git config --list 2.配置全局git用户名和邮箱 …...

ftp和sftp区别,以及xftp的使用

网上找链接找的很辛苦对吧&#xff01; 网上下载的破解版还不用。而且用没多久又说要更新了&#xff0c;又得重新找。 这下直接把官方免费获取链接发给你&#xff0c;就不用在被这种事情麻烦了。 家庭/学校免费 - NetSarang Website (xshell.com):家庭/学校免费 - NetSarang W…...

C++ 编程入门(一)—— Hello World

C 是什么环境搭建第一个 C 程序本篇结语 C 是什么 C 是一种面向对象的计算机程序设计语言&#xff0c;由美国 AT&T 贝尔实验室的 Bjarne Stroustrup 在 20 世纪 80 年代初期发明并实现&#xff08;最初这种语言被称作 “C with Classes” 带类的 C 语言&#xff09;。它是一…...

openlayers系列:加载arcgis和geoserver在线离线切片

https://www.freesion.com/article/1751396517/ 1.背景 有个项目需要使用openlayer加载各种服务上发布的数据&#xff0c;坐标系也不同&#xff0c;我们都知道openalyer默认可以加载EPAG:3857,要加载4490的坐标系的数据需要重新定义一下&#xff0c;之后再加载。一想起要重新…...

《人工智能安全》课程总体结构

1 课程内容 人工智能安全观&#xff1a;人工智能安全问题、安全属性、技术体系等基本问题进行了归纳整理。人工智能安全的主要数据处理方法&#xff0c;即非平衡数据分类、噪声数据处理和小样本学习。人工智能技术赋能网络空间安全攻击与防御&#xff1a;三个典型实例及攻击图…...

unity关于匀速移动某些值的方法

可能很多人会用到Verctor3.Lerp、Mathf.LerpUnclamped等等 这种其实不是匀速 看一下这个整体差不多的逻辑 public static float Lerp(float a, float b, float t){return a (b - a) * t;};这个逻辑就是&#xff0c;从a值到b值&#xff0c;返回一个a值加&#xff08;b值-a值&…...

解决VScode下载太慢的问题记录

最近突然想重新下载vscoded便携免安装版&#xff0c;发现下载很慢&#xff0c;于是乎查询一下&#xff0c;以便记录 下载地址 VScode官方网站&#xff1a; https://code.visualstudio.com/ 根据个人的需求选择下载&#xff0c;页面加载下载需要等一会&#xff0c; 然后就会…...

Gitlab服务器备份恢复及系统升级

居安思危&#xff0c;思则有备&#xff0c;有备无患。 基于此&#xff0c;申请了一个测试服务器&#xff0c;准备先安装同版本服务器&#xff0c;按照最新的数据进行恢复&#xff0c;然后再将现在的服务器升级到Gitlab的最新版本&#xff0c;记录一下完整的过程&#xff0c;以…...

docker入门讲解

目录 第 1 章 Docker核心概念与安装 为什么使用容器? Docker是什么 Docker设计目标 Docker基本组成 容器 vs 虚拟机 Docker应用场景 Linux 安装 Docker 第 2 章 Docker镜像管理 镜像是什么 镜像从哪里来? 镜像与容器联系 镜像常用管理命令 镜像存储核心技术:联…...

【Matlab】基于卷积神经网络的数据回归预测(Excel可直接替换数据))

【Matlab】基于卷积神经网络的数据回归预测(Excel可直接替换数据) 1.模型原理2.数学公式3.文件结构4.Excel数据5.分块代码6.完整代码7.运行结果1.模型原理 基于卷积神经网络(Convolutional Neural Network,CNN)的数据回归预测是一种常见的机器学习方法,适用于处理具有空…...

在Springboot集成Activiti工作流引擎-引入、调用,测试【基础讲解】

工作流 通过计算机对业务流程自动化执行管理 他主要解决的是使在多个参与者之间按照某种“预定义规则”自动进行传递稳定 信息或任务的过程 通俗来讲 业务上一个玩着的审批流程 比如请假&#xff0c;出差 外出采购等 工作流引擎就是来解决流程问题的 提高我们的工作效率 如果…...

Java书签 #解锁MyBatis的4种批量插入方式及ID返回姿势

1. 今日书签 项目开发中&#xff0c;我们经常会用到单条插入和批量插入。但是实际情况可能是&#xff0c;项目初期由于种种原因&#xff0c;在业务各处直接使用单条插入SQL进行开发&#xff08;未开启批处理&#xff09;&#xff0c;在后面的迭代中&#xff0c;系统性能问题渐…...

在react项目中如何引入国际化

react-i18next 在 React 项目中引入国际化&#xff08;Internationalization&#xff0c;简称 i18n&#xff09;可以使用第三方库来实现。其中&#xff0c;最常用且流行的国际化库是 react-i18next&#xff0c;它基于 i18next 实现&#xff0c;提供了方便易用的国际化功能。下…...

spring学习笔记十三

注解实现管理第三方Bean和为第三方Bean注入资源 1、添加pom坐标 <dependency><groupId>com.alibaba</groupId><artifactId>druid</artifactId><version>1.1.16</version></dependency> 2、SpringConfig配置类 Configuratio…...

react native 本地存储 AsyncStorage

An asynchronous, unencrypted, persistent, key-value storage system for React Native. Async Storage 只能用来储存字符串数据&#xff0c;所以为了去储存object类型的数据&#xff0c;得先进行序列化&#xff08;JSON.stringify()&#xff09;当你想要使用数据的时候&…...

Postgresql数据库中的时间类型汇总

PostgreSQL数据库有以下几种时间类型 1 日期 date&#xff1a;表示日期&#xff0c;格式为YYYY-MM-DD。 2 时间 time&#xff1a;表示时间&#xff0c;格式为HH:MI:SS。 3 日期和时间 timestamp&#xff1a;表示日期和时间&#xff0c;格式为YYYY-MM-DD HH:MI:SS。 4 带…...

算法刷题Day 51 最佳买卖股票时机含冷冻期+买卖股票的最佳时期含手续费

Day 51 动态规划 309. 最佳买卖股票时机含冷冻期 关键是要画出状态转移图 然后根据状态转移图来写状态转移方程 class Solution { public:int maxProfit(vector<int>& prices) {int len prices.size();vector<vector<int>> dp(len, vector<int&g…...

编程导航算法村 第五关 | 白银挑战

编程导航算法村 第五关 | 白银挑战 用栈实现队列 LeetCode 232题 class MyQueue {private Stack<Integer> stack; // 保存private Stack<Integer> tempstack; // 临沭队列public MyQueue() {stack new Stack<>();tempstack new Stack<>();}public…...

(十六十七)时序数据库是怎么存储用户名和密码的从InfluxDB OSS迁移数据

以下内容来自 尚硅谷&#xff0c;写这一系列的文章&#xff0c;主要是为了方便后续自己的查看&#xff0c;不用带着个PDF找来找去的&#xff0c;太麻烦&#xff01; 第 16 章 时序数据库是怎么存储用户名和密码的 1、InfluxDB内部自带了一个用Go语言写的BlotDB&#xff0c;Blo…...

5分钟开发一个AI论文抓取和ChatGPT提炼应用

5分钟开发一个AI论文抓取和ChatGPT提炼应用 第一步 点击“即刻开始” -选择模板 python -修改标题 “AIPaper”&#xff0c;项目标识“AIPaper”&#xff0c;点击“创建项目” 第二步 在编程区域右侧AI区域&#xff0c;输入框输入以下内容&#xff1a; 请根据下面的内容&…...

SK5代理与网络安全:保障爬虫隐匿性与HTTP连接稳定性

一、SK5代理简介 SK5代理&#xff0c;即socks5代理&#xff0c;是一种网络协议&#xff0c;用于在客户端和服务器之间进行数据传输。相比其他代理协议&#xff0c;如HTTP代理&#xff0c;SK5代理具有更高的性能和安全性&#xff0c;支持TCP和UDP连接&#xff0c;并可以处理更复…...

基于4G网络的嵌入式设备远程升级系统设计与实现(学习一)

摘要 随着无线通信技术的不断更新发展&#xff0c;嵌入式设备的联网应用领域得以大规模扩大&#xff0c;远程升级功能成为产品开发中必不可少的一部分。 本文对嵌入式设备远程升级进行了研究&#xff0c;在不改变设备硬件集成度基础上&#xff0c;设计实现了分离式升级的远程…...

陪诊小程序软件|陪诊系统定制|医院陪诊小程序

开发一个陪诊小程序需要投入一定的费用&#xff0c;具体金额会因项目的复杂程度、功能需求和推广政策而有所差异在投入资金之前&#xff0c;建议进行市场调研和需求分析&#xff0c;制定出合理的预算&#xff0c;并选择专业的开发团队进行合作&#xff0c;那么开发陪诊小程序需…...

[数据集][目标检测]空中飞鸟目标检测数据集VOC格式4955张

数据集名称&#xff1a;空中飞鸟数据集VOC-4955张 数据集制作单位&#xff1a;未来自主研究中心(FIRC) 图片数量(jpg文件个数)&#xff1a;4955 标注数量(xml文件个数)&#xff1a;4955 标注类别数&#xff1a;1 标注类别名称:["bird"] 每个类别标注的框数&#xff1…...

安徽现货黄金代理请看这篇

持续两三年的新冠疫情&#xff0c;令全球经济遭受不同程度的打击&#xff0c;很多传统的行业更是重灾区&#xff0c;当中不少从业多年的朋友表示虽然看不清前进&#xff0c;但也不敢随便转行&#xff0c;如果那么有一份这样的工作&#xff0c;既不用他们离开本职&#xff0c;也…...

HTML JS实现点击按钮下载文件功能例子(C知道版)

其实这篇应该算是一篇“水”文章&#xff0c;为什么要这么“水”呢&#xff0c;除了最近南方的气候闷热难耐需要降温之外&#xff0c;另一个主要原因&#xff0c;这里面所写的代码均是由CSDN的AI文本大模型"C知道"完成&#xff0c;我在这里只是简单记录一下&#xff…...