RV1126+FFMPEG推流项目(11)编码音视频数据 + FFMPEG时间戳处理
本节介绍本章节主要讲解的是push_server_thread线程的具体处理流程, push_server_thread这个线程的主要功能是通过时间戳比较,来处理音频、视频的数据并最终推流到SRT、RTMP、UDP、RTSP服务器
push_server_thread:流程如下
上图,主要阐述了push_server_thread的工作流程,因为这个线程主要处理的是通过时间戳进行比较(av_compare_ts)。若检测到音频时间戳则处理音频数据,若检测到视频时间戳则处理视频数据,最终把音视频数据合成到TS、FLV并推流到RTMP、SRT、UDP、RTSP服务器。
上图, 是视频编码时间戳、音频编码时间戳经过了时间基转换后的具体数值:视频时间基成video_time_base = {1,25},音频时间基audio_time_base = {1,48000}转换成TS后:视频PTS = {0,3600,7200,10800,14400,18000…},音频PTS = {0, 1920,3840,5760,7680,9600…}。
这里要注意的是:
- 在这个推流项目中视频帧率和时间基固定成video_time_base = {1,25},video_frame_rate = {1,25}。因为底层驱缘故,易百纳的摄像头帧率可能只支持25帧,所以编码帧率和时间基只能设置{1,25},否则就会导致音视频不同步。
- 视频VIDEO_PTS和音频AUDIO_PTS,需要按照一定的数值规律进行累加。中间不能出现任何的丢失和错误,否则就会出现各种问题,如花屏、卡顿、音视频不同步等问题。 比方说:video_pts = {0,3600,7200,,14400}这种属于PTS出现丢失;
-
push_server_thread线程模块讲解:
// 音视频合成推流线程
/*** @brief 推送服务器线程的入口函数* * 该函数负责在一个独立的线程中处理音视频数据的推送任务。* 它通过比较视频和音频的时间戳来决定下一个要处理的数据类型,* 以确保音视频同步。此外,它还负责释放相关的资源。* * @param args 传递给线程的参数,这里是FFMPEG的配置信息* @return void* 返回线程的退出状态*/
void *push_server_thread(void *args)
{// 确保线程可以独立运行,即使父线程结束,该线程也不会变为僵死状态pthread_detach(pthread_self());// 将传递给线程的参数转换为所需的结构体类型RKMEDIA_FFMPEG_CONFIG ffmpeg_config = *(RKMEDIA_FFMPEG_CONFIG *)args;// 释放传递给线程的参数内存free(args);// 初始化AVOutputFormat指针AVOutputFormat *fmt = NULL;// 初始化返回值变量int ret;// 无限循环,处理音视频数据while (1){/*我们以转换到同一时基下的时间戳为例,假设上一时刻音、视频帧的保存时间戳都是0。当前任意保存一种视频帧,例如保存视频的时间戳为video_t1。接着比较时间戳,发现音频时间戳为0 < video_t1,保存一帧音频,时间戳为audio_t1。继续比较时间戳,发现audio_t1 < video_t1,选择保存一帧音频,时间戳为audio_t2。再一次比较时间戳video_t1 < audio_t2,选择保存一帧视频,时间戳为video_t2。int av_compare_ts(int64_t ts_a, AVRational_tb_b,int64_t ts_b, AVRational tb_b){int64_t a = tb_a.num * (int64_t)tb_b.den;int64_t b = tb_b.num * (int64_t)tb_a.den;if ((FFABS64U(ts_a)|a|FFABS64U(ts_b)|b) <= INT_MAX)return (ts_a*a > ts_b*b) - (ts_a*a < ts_b*b);if (av_rescale_rnd(ts_a, a, b, AV_ROUND_DOWN) < ts_b)return -1;if (av_rescale_rnd(ts_b, b, a, AV_ROUND_DOWN) < ts_a)return -1;return 0;}*/// 比较视频和音频的时间戳,决定下一个要处理的数据类型ret = av_compare_ts(ffmpeg_config.video_stream.next_timestamp,ffmpeg_config.video_stream.enc->time_base,ffmpeg_config.audio_stream.next_timestamp,ffmpeg_config.audio_stream.enc->time_base);// 如果视频时间戳小于等于音频时间戳,处理视频数据if (ret <= 0){ret = deal_video_avpacket(ffmpeg_config.oc, &ffmpeg_config.video_stream); // 处理FFMPEG视频数据if (ret == -1){printf("deal_video_avpacket error\n");break;}}else // 否则,处理音频数据{ret = deal_audio_avpacket(ffmpeg_config.oc, &ffmpeg_config.audio_stream); // 处理FFMPEG音频数据if (ret == -1){printf("deal_video_avpacket error\n");break;}}}// 写入AVFormatContext的尾巴av_write_trailer(ffmpeg_config.oc);// 释放VIDEO_STREAM的资源free_stream(ffmpeg_config.oc, &ffmpeg_config.video_stream);// 释放AUDIO_STREAM的资源free_stream(ffmpeg_config.oc, &ffmpeg_config.audio_stream);// 释放AVIO资源avio_closep(&ffmpeg_config.oc->pb);// 释放AVFormatContext资源avformat_free_context(ffmpeg_config.oc);return NULL;
}
上面的代码就是push_server_thread线程的主要工作, 从上面的的代码可以分析到av_compare_ts去进行每一帧时间戳的比较。我们设定用ts_a和tb_a作为视频的时间戳和时间基、ts_b和tb_b作为音频的时间戳和时间基。若ret(返回值)<=0,则说明此时要处理视频编码数据,就调用deal_video_avpacket函数进行视频编码数据的写入;否则就调用deal_audio_avpacket进行音频编码数据的写入,当这个线程退出后, 先av_write_trailer结束写入文件结束符,并释放所有的资源数据(free_stream、avio_closp、avforamt_free_context)。
av_compare_ts的作用:
把音视频的顺序弄正确,防止解码端解码端出错。它的主要作用是进行时间戳进行实时比较,它能够实时保证当前的时间戳是,准确无误的。它不会出现时间戳混乱的情况,所谓混乱的情况就相当于:视频时间戳当成音频时间戳处理,音频时间戳当成视频时间戳处理。
push_server_thread线程,里面最重要的两个函数 deal_video_avpacket和deal_audio_avpacket
deal_video_avpacket:
/*** 处理视频AVPacket,将其写入到复合流中* * @param oc AVFormatContext指针,表示复合流的上下文* @param ost OutputStream指针,包含编码和流信息* @return 成功返回0,失败返回-1*/
int deal_video_avpacket(AVFormatContext *oc, OutputStream *ost)
{int ret;AVCodecContext *c = ost->enc; // 获取编码器上下文AVPacket *video_packet = get_ffmpeg_video_avpacket(ost->packet); // 从RV1126视频编码数据赋值到FFMPEG的Video AVPacket中if (video_packet != NULL){video_packet->pts = ost->next_timestamp++; // VIDEO_PTS按照帧率进行累加}ret = write_ffmpeg_avpacket(oc, &c->time_base, ost->stream, video_packet); // 向复合流写入视频数据if (ret != 0){printf("write video avpacket error");return -1;}return 0;
}
deal_video_avpacket函数里面主要包含了以下重要的功能:
第一步:通过get_ffmpeg_video_avpacket函数里面,从视频队列中获取视频编码数据,并把视频数据赋值到AVPacket里面(这里很重要,因为我们最终推流用的都是AVPacket结构体数据)。
get_ffmpeg_video_avpacket:
AVPacket *get_ffmpeg_video_avpacket(AVPacket *pkt)
{video_data_packet_t *video_data_packet = video_queue->getVideoPacketQueue(); // 从视频队列获取数据if (video_data_packet != NULL){
/*重新为FFMPEG的Video AVPacket分配给定的缓冲区1. 如果入参的 AVBufferRef 为空,直接调用 av_realloc 分配一个新的缓存区,并调用 av_buffer_create 返回一个新的 AVBufferRef 结构;2. 如果入参的缓存区长度和入参 size 相等,直接返回 0;3. 如果对应的 AVBuffer 设置了 BUFFER_FLAG_REALLOCATABLE 标志,或者不可写,再或者 AVBufferRef data 字段指向的数据地址和 AVBuffer 的 data 地址不同,递归调用 av_buffer_realloc 分配一个新的 buffer,并将 data 拷贝过去;4. 不满足上面的条件,直接调用 av_realloc 重新分配缓存区。*/int ret = av_buffer_realloc(&pkt->buf, video_data_packet->video_frame_size + 70);if (ret < 0){return NULL;}pkt->size = video_data_packet->video_frame_size; // rv1126的视频长度赋值到AVPacket Sizememcpy(pkt->buf->data, video_data_packet->buffer, video_data_packet->video_frame_size); // rv1126的视频数据先拷贝到ptk->buf->data中pkt->data = pkt->buf->data; // 把pkt->buf->data赋值到pkt->data,如果直接赋给pkt->data,会报错pkt->flags |= AV_PKT_FLAG_KEY; // 默认flags是AV_PKT_FLAG_KEY,关键帧,如果没有回黑屏if (video_data_packet != NULL){free(video_data_packet); //释放掉内存video_data_packet = NULL;}//已经把视频队列里面的数据已经拷贝到了ffmpeg的packet的data中。return pkt; //返回一个指针,指向ffmpeg的packet的data,因为我们最终推流用的都是AVPacket结构体数据}else{return NULL; //队列里面没有数据了,}
}
这里需要注意的有两个地方:
在AVPacket中buf的赋值,不能够直接赋值,如: memcpy(pkt->data, video_data_packet->buffer, video_data_packet->frame_size)否则程序就会出现core_dump情况。我们需要先把video_data_packet_t的视频数据(video_data_packet->buffer)先拷贝到pkt->buf->data,然后再把pkt->buf->data的数据赋值到pkt->data。
memcpy(pkt->buf->data, video_data_packet->buffer, video_data_packet->video_frame_size); // rv1126的视频数据先拷贝到ptk->buf->data中pkt->data = pkt->buf->data;
对于视频的AVPacket中,需要对它的标识符flag进行关键帧设置(pkt->flags |= AV_PKT_FLAG_KEY),否则解码端则无法正常播放视频。代码如下:
pkt->flags |= AV_PKT_FLAG_KEY; // 默认flags是AV_PKT_FLAG_KEY,关键帧,如果没有会没有办法播放,黑屏
第二步:根据AVPacket的数据去计算视频的PTS,若AVPacket的数据不为空。则让视频video_packet->pts = ost->next_timestamp++; (关于video的PTS计算,上一篇已经聊过了)。
第三步:write_ffmpeg_avpacket:把视频PTS进行时间基的转换,调用av_packet_rescale_ts把采集的视频时间基转换成复合流的时间基。时间基转换完成之后,就把视频数据写入到复合流文件里面,调用的API是av_interleaved_write_frame (注意:复合流文件可以是本地文件也可以是流媒体地址)。
/*** 写入FFmpeg视频数据包* * 此函数负责将一个AVPacket中的数据写入到视频文件中在写入之前,它会根据提供的time_base和流的time_base调整AVPacket的时间戳* 这是为了确保时间戳匹配流的时基,防止播放时出现同步问题* * @param fmt_ctx FFmpeg格式上下文,用于写入数据* @param time_base 指向AVRational的指针,表示时间基数* @param st 视频流,用于确定stream_index* @param pkt 包含编码视频数据的AVPacket* @return 返回av_interleaved_write_frame的结果,表示写入操作是否成功*/
int write_ffmpeg_avpacket(AVFormatContext *fmt_ctx, const AVRational *time_base, AVStream *st, AVPacket *pkt)
{/*将输出数据包时间戳值从编解码器重新调整为流时基 */av_packet_rescale_ts(pkt, *time_base, st->time_base);pkt->stream_index = st->index;// 向复合流写入视频数据,复合流文件可以是本地文件也可以是流媒体地址return av_interleaved_write_frame(fmt_ctx, pkt);
}
deal_audio_avpacket的实现:流程和视频的基本一样
int deal_audio_avpacket(AVFormatContext *oc, OutputStream *ost)
{int ret;AVCodecContext *c = ost->enc;AVPacket *audio_packet = get_ffmpeg_audio_avpacket(ost->packet); // 从RV1126视频编码数据赋值到FFMPEG的Audio AVPacket中if (audio_packet != NULL){audio_packet->pts = ost->samples_count;ost->samples_count += 1024;ost->next_timestamp = ost->samples_count; // AUDIO_PTS按照帧率进行累加1024}ret = write_ffmpeg_avpacket(oc, &c->time_base, ost->stream, audio_packet); // 向复合流写入音频数据if (ret != 0){printf(" write audio avpacket error");return -1;}return 0;
}
deal_audio_avpacket函数里面主要包含了以下重要的功能:
第一步:通过get_ffmpeg_audio_avpacket函数里面,从音频队列中获取音频编码数据,并把音频数据赋值到AVPacket里面(这里很重要,因为我们最终推流用的都是AVPacket结构体数据)。具体的赋值如下图:
AVPacket *get_ffmpeg_audio_avpacket(AVPacket *pkt)
{audio_data_packet_t *audio_data_packet = audio_queue->getAudioPacketQueue();// 从音频队列获取数据if (audio_data_packet != NULL){/*重新分配给定的缓冲区
1. 如果入参的 AVBufferRef 为空,直接调用 av_realloc 分配一个新的缓存区,并调用 av_buffer_create 返回一个新的 AVBufferRef 结构;
2. 如果入参的缓存区长度和入参 size 相等,直接返回 0;
3. 如果对应的 AVBuffer 设置了 BUFFER_FLAG_REALLOCATABLE 标志,或者不可写,再或者 AVBufferRef data 字段指向的数据地址和 AVBuffer 的 data 地址不同,递归调用 av_buffer_realloc 分配一个新
的 buffer,并将 data 拷贝过去;
4. 不满足上面的条件,直接调用 av_realloc 重新分配缓存区。
*/int ret = av_buffer_realloc(&pkt->buf, audio_data_packet->audio_frame_size + 70);if (ret < 0){return NULL;}pkt->size = audio_data_packet->audio_frame_size; // rv1126的音频长度赋值到AVPacket Sizememcpy(pkt->buf->data, audio_data_packet->buffer, audio_data_packet->audio_frame_size); //rv1126的音频数据赋值到AVPacket datapkt->data = pkt->buf->data; // 把pkt->buf->data赋值到pkt->dataif (audio_data_packet != NULL){free(audio_data_packet);audio_data_packet = NULL;}return pkt;}else{return NULL;}
}
我们来分析音频AVPacket如何赋值:
第一步:在AVPacket中buf的赋值,不能够直接赋值,如: memcpy(pkt->data, audio_data_packet->buffer, audio_data_packet->frame_size)否则程序就会出现core_dump情况。我们需要先把audio_data_packet_t的视频数据(audio_data_packet->buffer)先拷贝到pkt->buf->data,然后再把pkt->buf->data的数据赋值到pkt->data。
第二步:根据AVPacket的数据去计算音频的PTS,若音频AVPacket的数据不为空。则对音频PTS进行计算,计算公式如下:
audio_packet->pts = ost->samples_count;
ost->samples_count += 1024;
ost->next_timestamp = ost->samples_count; // AUDIO_PTS按照帧率进行累加1024
(关于audio的PTS计算是每次累加1024,上一节课已经讲了)。
第三步:和视频一样把音频PTS进行时间基的转换,调用av_packet_rescale_ts把采集的音频时间基转换成复合流的时间基。时间基转换完成之后,就把音频数据写入到复合流文件里面,调用的API是同样也是av_interleaved_write_frame (注意:复合流文件可以是本地文件也可以是流媒体地址)。
最后一步释放资源:
void *push_server_thread(void *args)
{// 确保线程可以独立运行,即使父线程结束,该线程也不会变为僵死状态pthread_detach(pthread_self());// 将传递给线程的参数转换为所需的结构体类型RKMEDIA_FFMPEG_CONFIG ffmpeg_config = *(RKMEDIA_FFMPEG_CONFIG *)args;// 释放传递给线程的参数内存free(args);// 初始化AVOutputFormat指针AVOutputFormat *fmt = NULL;// 初始化返回值变量int ret;// 无限循环,处理音视频数据while (1){/*我们以转换到同一时基下的时间戳为例,假设上一时刻音、视频帧的保存时间戳都是0。当前任意保存一种视频帧,例如保存视频的时间戳为video_t1。接着比较时间戳,发现音频时间戳为0 < video_t1,保存一帧音频,时间戳为audio_t1。继续比较时间戳,发现audio_t1 < video_t1,选择保存一帧音频,时间戳为audio_t2。再一次比较时间戳video_t1 < audio_t2,选择保存一帧视频,时间戳为video_t2。int av_compare_ts(int64_t ts_a, AVRational_tb_b,int64_t ts_b, AVRational tb_b){int64_t a = tb_a.num * (int64_t)tb_b.den;int64_t b = tb_b.num * (int64_t)tb_a.den;if ((FFABS64U(ts_a)|a|FFABS64U(ts_b)|b) <= INT_MAX)return (ts_a*a > ts_b*b) - (ts_a*a < ts_b*b);if (av_rescale_rnd(ts_a, a, b, AV_ROUND_DOWN) < ts_b)return -1;if (av_rescale_rnd(ts_b, b, a, AV_ROUND_DOWN) < ts_a)return -1;return 0;}*/// 比较视频和音频的时间戳,决定下一个要处理的数据类型ret = av_compare_ts(ffmpeg_config.video_stream.next_timestamp,ffmpeg_config.video_stream.enc->time_base,ffmpeg_config.audio_stream.next_timestamp,ffmpeg_config.audio_stream.enc->time_base);// 如果视频时间戳小于等于音频时间戳,处理视频数据if (ret <= 0){ret = deal_video_avpacket(ffmpeg_config.oc, &ffmpeg_config.video_stream); // 处理FFMPEG视频数据if (ret == -1){printf("deal_video_avpacket error\n");break;}}else // 否则,处理音频数据{ret = deal_audio_avpacket(ffmpeg_config.oc, &ffmpeg_config.audio_stream); // 处理FFMPEG音频数据if (ret == -1){printf("deal_video_avpacket error\n");break;}}}// 写入AVFormatContext的尾巴av_write_trailer(ffmpeg_config.oc);// 释放VIDEO_STREAM的资源free_stream(ffmpeg_config.oc, &ffmpeg_config.video_stream);// 释放AUDIO_STREAM的资源free_stream(ffmpeg_config.oc, &ffmpeg_config.audio_stream);// 释放AVIO资源avio_closep(&ffmpeg_config.oc->pb);// 释放AVFormatContext资源avformat_free_context(ffmpeg_config.oc);return NULL;
}
avcodec_close:关闭编码器
avcodec_free_context:释放解码器上下文
av_buffer_unref:将当前的AVBufferRef指针指向的内存释放,并对AVBufferRef指向的数据引用计数减1
av_packet_unref:对AVPacket进行清理
av_packet_free:释放AVPacket所有资源
avio_closep:关闭输出文件IO
avformat_free_context:销毁AVFormatContext结构体
相关文章:

RV1126+FFMPEG推流项目(11)编码音视频数据 + FFMPEG时间戳处理
本节介绍本章节主要讲解的是push_server_thread线程的具体处理流程, push_server_thread这个线程的主要功能是通过时间戳比较,来处理音频、视频的数据并最终推流到SRT、RTMP、UDP、RTSP服务器 push_server_thread:流程如下 上图,…...

人工智能的出现,给生命科学领域的研究带来全新的视角|行业前沿·25-01-22
小罗碎碎念 今天和大家分享一份白皮书,系统总结并陈述人工智能在生命科学领域的应用。 人工智能在生命科学领域的应用,具体包括——单细胞转录组、疾病诊疗、医疗文本处理、RNA结构预测等多个方面,通过这份报告,我们可以详细了解相…...

python注释格式总结
三个双引号的用于文件,类,函数注释。 没有统一的规定,以下是比较清晰的写法。 文件注释(文件顶部):文件用途空行作者信息(每行一个键:值) 类注释(类名下行)…...

Django实现数据库的表间三种关系
Django实现数据库的表间三种关系 1. 一对多(One-to-Many)关系示例:关系说明:查询示例: 2. 一对一(One-to-One)关系示例:关系说明:查询示例: 3. 多对多&#x…...

C++蓝桥真题讲解
本篇文章和大家一起来试试一些简单的蓝桥真题 注意:本篇文章将全程使用devc和蓝桥官网,如果有小伙伴找不到devc安装包的可以本篇文章中下载。 赛前必知点 1.正式比赛时,先从蓝桥官网下载题目文档,然后用devc进行编译,…...

【21】Word:德国旅游业务❗
目录 题目 NO1.2.3 NO4 NO5.6 NO7 NO8.9.10.11 题目 NO1.2.3 F12:另存为布局→页面设置→页边距:上下左右选中“德国主要城市”→开始→字体对话框→字体/字号→文本效果:段落对话框→对齐方式/字符间距/段落间距 NO4 布局→表对话框…...

如何分辨ddos攻击和cc攻击?
DDoS(分布式拒绝服务)攻击和 CC(Challenge Collapsar)攻击都属于网络攻击手段,主要通过消耗目标服务器资源使其无法正常提供服务,但它们在攻击原理、攻击特征等方面存在区别: 攻击原理 DDoS 攻…...

enum EPOLL_EVENTS详解
enum EPOLL_EVENTS 是 Linux 中 epoll 机制的核心定义之一,它定义了 epoll 支持的所有事件类型。每个事件类型对应一个唯一的位掩码(bitmask),通过按位或(|)可以组合多个事件类型,通过按位与&am…...

阿里前端开发规范
文章目录 1. 为什么前端写代码要规范?一、代码规范的必要性二、 规范带来的好处 2. 资源一、推荐 1. 为什么前端写代码要规范? 一、代码规范的必要性 可维护性 统一的代码风格便于理解和修改减少代码维护成本降低项目交接难度 团队协作 提高团队开发效…...

从函数式编程到响应式编程:现代开发中的范式转变
引言 随着软件开发领域的不断进化,编程范式也在经历着一场又一场的变革。从面向过程到面向对象,再到近年来流行的函数式编程和响应式编程,开发者正逐步适应不同的编程思想来解决现代软件开发中的复杂问题。本文将带你了解函数式编程和响应式编…...

Django学习笔记(启动项目)-03
Django学习笔记(启动项目)-03 1、在urls文件中配置一个路由url 2、在views文件中创建视图函数 3、启动项目测试结果 # 输入项目启动命令 python manage.py runserver4、创建HTML模版和静态文件 1、在templates文件夹中创建一个html 2、创建url路由与视图函数 3、测试效果 4、…...

量变引起质变
量变引起质变,这个是最本质的规律,重复进行一件事情,这件事情就会越来越完善,越来越完美,哪怕是菜鸟,重复多了就是大佬。 我从说话结结巴巴,到说话流畅,只是用了15天直播写代码&…...

NewStar CTF week1 web wp
谢谢皮蛋 做这题之前需要先去学习一些数据库的知识 1 order by 2 1可以理解为输入的id,是一个占位符,按第二列排序用来测试列数,如果没有两列则会报错-1 union select 1,2 -1同样是占位符,union的作用是将注入语句合并到原始语句…...

李沐vscode配置+github管理+FFmpeg视频搬运+百度API添加翻译字幕
终端输入nvidia-smi查看cuda版本 我的是12.5,在网上没有找到12.5的torch,就安装12.1的。torch,torchvision,torchaudio版本以及python版本要对应 参考:https://blog.csdn.net/FengHanI/article/details/135116114 创…...

深度学习中Batch Normalization(BN)原理、作用浅析
最近做剪枝学习,其中一种是基于BN层的γ作为缩放因子进行剪枝的,那么我想搞懂BN的工作原理更好的理解网络、剪枝等,所以有了该文。 首先先说BN的作用在详细拆解,理解。以知乎一条高赞评论说明BN层到底在干什么。 Batch Norm 为什…...

C语言常用字符串处理函数
头文件:#include <string.h> strlen size_t strlen( char *str ); 功能:函数返回一个整数值,表示给定字符串的长度(不包括结束符\0) strcat char *strcat( char *str1, const char *str2 ); 功能&a…...

文件上传漏洞详解
第一关(JS绕过) 1.1使用bp进行绕过 先将要上传的php文件的后缀改为png,然后在上传时抓包,将png后缀再改为php,发包,此时上传成功 1.2使用js进行绕过 打开浏览器的检查,将其中的checkFile函数…...

关于linux的ld.so.conf.d
初级代码游戏的专栏介绍与文章目录-CSDN博客 我的github:codetoys,所有代码都将会位于ctfc库中。已经放入库中我会指出在库中的位置。 这些代码大部分以Linux为目标但部分代码是纯C的,可以在任何平台上使用。 源码指引:github源…...

pytest执行报错:found no collectors
今天在尝试使用pytest运行用例的时候出现报错:found no collectors;从两个方向进行排查,一是看文件名和函数名是不是符合规范,命名要是"test_*"格式;二是是否存在修改文件名的情况,如果修改过文件…...

如何实现网页不用刷新也能更新
要实现用户在网页上不用刷新也能到下一题,可以使用 前端和后端交互的技术,比如 AJAX(Asynchronous JavaScript and XML)、Fetch API 或 WebSocket 来实现局部页面更新。以下是一个实现思路: 1. 使用前端 AJAX 或 Fetch…...

c#调用c++的dll,字符串指针参数问题
一.背景 在 C# 中没有 char* 类型,因为 C# 是一种托管语言,它的设计目标是提供更高级别的安全性和内存管理,避免使用像 C 或 C 中的指针操作,以防止常见的指针相关错误,如内存泄漏和悬空指针。 二.c#调用c的dll&#x…...

HTML5 新表单属性详解
HTML5 为 <form> 和 <input> 标签引入了一系列新属性,极大地增强了表单的功能和用户体验。这些新属性不仅简化了开发者的工作,还为用户提供了更友好、更高效的交互方式。本文将详细介绍这些新属性,并结合代码示例帮助大家更好地理…...

JAVA 使用反射比较对象属性的变化,记录修改日志。使用注解【策略模式】,来进行不同属性枚举值到中英文描述的切换,支持前端国际化。
1.首先定义一个接口,接口中有两个方法,分别是将属性转换成英文描述和中文描述。 其实就是将数据库中记录的 0 1 ,转换成后面的描述 这边定义了中文转换为默认方法,是因为有些属性不需要进行中文转换,或者该属性的枚举…...

Docker入门学习
一、容器 1. 将单个操作系统中的资源划分到孤立的组中,在孤立的组中平衡有冲突的资源使用需求 2. Docker提供了容器管理的工具可以无需关注底层操作,使用效果类似于轻量级的虚拟机, 并且容器的创建和停止相对于虚拟机来说比较快&am…...

吴恩达深度学习——神经网络介绍
文章内容来自BV11H4y1F7uH,仅为个人学习所用。 文章目录 什么是神经网络引入神经网络神经元激活函数ReLU隐藏单元 用神经网络进行监督学习监督学习与无监督学习举例 什么是神经网络 引入 已经有六个房子的数据集,横轴为房子大小,纵轴为房子…...

STM32之CubeMX新建工程操作(十八)
STM32F407 系列文章 - STM32CubeMX(十八) 目录 前言 一、STM32CubeMX 二、新建工程 编辑 1.创建工程 2.选择芯片型号 3.Pinout引脚分配 1.SYS配置 2.RCC配置 3.定时器配置 4.GPIO引脚配置 5.中断配置 6.通讯接口配置 7.插件Middleware配…...

Postgresql源码(140)理解PG的编译流程(make、Makefile、Makefile.global.in)
PG16 PG中使用的makefile看起来代码比较多,但是实际逻辑比较简单,这里做一些抽象总结。 总结 Makefile.global.in的$(recurse)宏自动生成了target,可以方便的进入内存目录进行编译。 all: all-common-recurse all-common-recurse: submak…...

logback日志自定义占位符
前言 在大型系统运维中,很大程度上是需要依赖日志的。在java大型web工程中,一般都会使用slf4jlogback这一个组合来实现日志的管理。 logback中很多现成的占位符可以可以直接使用,比如线程号【%t】、时间【%d】、日志等级【%p】,…...

Vue平台开发三——项目管理页面
前言 对于多个项目的使用,可能需要进行项目切换管理,所以这里创建一个项目管理页面,登录成功后跳转这个页面,进行选择项目,再进入Home页面展示对应项目的内容。 一、实现效果图预览 二、页面内容 功能1、项目列表展…...

用于牙科的多任务视频增强
Multi-task Video Enhancement for Dental Interventions 2022 miccai Abstract 微型照相机牢牢地固定在牙科手机上,这样牙医就可以持续地监测保守牙科手术的进展情况。但视频辅助牙科干预中的视频增强减轻了低光、噪音、模糊和相机握手等降低视觉舒适度的问题。…...