用现代 C++ 封装 FFmpeg:从摄像头采集到 H.264 编码的完整实践

一、前言

        在音视频开发领域,ffmpeg是常用的标准库,使用中用到大量的内存管理,不过结合c++对其封装,可以不在过度操心于内存管理,而更关注于处理逻辑。小编这次做的是使用ffmpeg的标准库在虚拟机上(ubuntu20.04)启用摄像头录制视频,保存的录制文件是mp4格式。

        ffmpeg类似于胶水,在linux上底层实际上是通过v4l2跟内核通信从而驱动摄像头,后续小编有时间再出一个比较大的物体检测的项目,到时候会直接使用v4l2,不在通过ffmpeg,因为对于资源受限的嵌入式设备来说,ffmpeg还是比较庞大的(v4l2仅仅几kb,而ffmpeg则多达几mb),同时v4l2支持直接内存映射(mmap),无需拷贝数据,这对检测物体的实时性比较友好。

        废话不再多说,我们回归整体。小编这里顺嘴带一下摄像头的工作原理,光线进入透镜成缩小倒立的像(也就是初中的物理学凸透镜成像)打到图像传感器(CMOS或CCD),传感器表面覆盖着由红、绿、蓝滤色片组成的拜耳阵列,每个像素点根据接收到的光照强度产生相应的电荷,经模数转换和色彩插值处理后生成完整的数字图像,最终通过接口输出。其实这里面用到了好多算法(白平衡、伽马校正、降噪等)来优化图像质量,我们只需了解一下即可。

        我们先了解一下ffmpeg几个比较重要的框架:libavformat(封装/解封装)、libavcodec(编解码)、libswscale/libswresample(图像/音频重采样)、libavdevice(设备)、libavutil(工具库)等,这也是本节用到的几个库。

二、架构流程

        小编这次的小实验讲逻辑拆分为:设备打开、解码器创建、编码器创建、输出流配置、工具初始化、帧处理,这种架构模式非常适合后期的扩展,比如加入音频、rtmp推流、多路输入等等。下面是核心类(CaptureController):

static std::string av_err2str_cpp(int errnum) { char buf[AV_ERROR_MAX_STRING_SIZE]; return av_strerror(errnum, buf, AV_ERROR_MAX_STRING_SIZE) ? std::string("Unknown error") : std::string(buf); } /* 编解码器智能指针 */ struct AVCodecContextDelete { void operator()(AVCodecContext *ctx) const { if(ctx) avcodec_free_context(&ctx); } }; using AVCodecContextPtr = std::unique_ptr<AVCodecContext, AVCodecContextDelete>; struct AVFrameDelete { void operator()(AVFrame *frame) const { if(frame) av_frame_free(&frame); } }; using AVFramePtr = std::unique_ptr<AVFrame, AVFrameDelete>; class CaptureController { public: enum encoder_mode_t{ ENCODER_MODE_REALTIME_STREAM, ENCODER_MODE_FIXED_BITRATE, ENCODER_MODE_HIGH_QUALITY, ENCODER_MODE_END }; ~CaptureController(); int camera_open(const char *iurl, int w, int h, int framerate); int dencoder_create(); int video_output_open(const char *ourl); int encoder_create(int w, int h, int framterate, int bit_rate, encoder_mode_t mode); int ostream_create(); int init_utils(); int process_frame(int64_t start_time, int64_t captrue_time); int flush_frame(); int write_header(const char *ourl) { if(!(videoOutput.ofmt_.get()->oformat->flags & AVFMT_NOFILE)) { int ret = avio_open(&videoOutput.ofmt_.get()->pb, ourl, AVIO_FLAG_WRITE); if(ret < 0) { av_log(NULL, AV_LOG_ERROR, "avio_open failed: [%d]%s\n", ret, av_err2str_cpp(ret).c_str()); return ret; } } return avformat_write_header(videoOutput.ofmt_.get(), NULL); } int write_trailer() { return av_write_trailer(videoOutput.ofmt_.get()); } int read_frame() { int ret = av_read_frame(cameraSource.ifmt_.get(), utils.pkt_.get()); if(ret < 0) { av_log(NULL, AV_LOG_ERROR, "av_read_frame failed: [%d]%s\n", ret, av_err2str_cpp(ret).c_str()); return -1; } if(utils.pkt_.get()->stream_index != cameraSource.video_stream) { av_packet_unref(utils.pkt_.get()); return 0; } return 1; } void set_lastpts(int64_t pts) { utils.last_pts = pts; } int64_t get_lastpts() { return utils.last_pts; } void set_lastdts(int64_t dts) { utils.last_dts = dts; } int64_t get_lastdts() { return utils.last_dts; } private: int init_sws(bool has_denc); void swscale_format(); int encoder_and_write_file(int64_t pts); int pkt_write_file(); struct { std::unique_ptr<AVFormatContext, void (*)(AVFormatContext *)> ifmt_ { /* 输入上下文 */ nullptr, [](AVFormatContext *ifmt) { if(ifmt) avformat_close_input(&ifmt); } }; AVCodecContextPtr denc_ctx_; /* 解码器 */ AVCodec *denc = NULL; int video_stream = -1; /* 视频流 */ } cameraSource; struct { std::unique_ptr<AVFormatContext, void (*)(AVFormatContext *)> ofmt_ { /* 输出上下文 */ nullptr, [](AVFormatContext *ofmt) { if(ofmt) avformat_free_context(ofmt); } }; AVCodec *enc = NULL; AVCodecContextPtr enc_ctx_; /* 编码器 */ AVStream *ostream = NULL; /* 输出视频流 */ } videoOutput; struct { AVFramePtr iframe; /* 中间帧,只负责指向原始数据 */ AVFramePtr frame; /* 实际帧 */ std::unique_ptr<AVPacket, void (*)(AVPacket *)> pkt_ { nullptr, [](AVPacket *pkt){ if(pkt) av_packet_free(&pkt); } }; std::unique_ptr<struct SwsContext, void (*)(struct SwsContext *)> sws_ctx_ { nullptr, [](struct SwsContext *sws_ctx) { if(sws_ctx) sws_freeContext(sws_ctx); } }; int64_t last_pts = -1, last_dts = -1; bool sws_initd = false; } utils; };

        这里需要提示几点。首先就是我们需要自封装一个出错接口,如果在c中我们会直接使用av_err2str进行打印出错信息,但是c++不可以,我们看源代码:

/* 这种写法在c中是合法的,但是c++对类型检测比较严格,直接使用会报错, 所以建议跟小编一样封装一个接口 */ #define av_err2str(errnum) \ av_make_error_string((char[AV_ERROR_MAX_STRING_SIZE]){0}, AV_ERROR_MAX_STRING_SIZE, errnum)

三、模块

首先我们看一下摄像头模块,也就是类中的cameraSource结构体,主要用于读取摄像头的数据。

(1)打开设备:

        这里需要调用avdevice_register_all去注册一下设备,仅仅我们在调用外部摄像头需要,比我我们单纯的像操作某个视频是不需要的。这里还有一点需要注意的就是我们虽然对摄像头进行参数设置,但是我们要清楚,这个设置是“尝试”,也就是说如果设置的某个参数摄像头不支持那么不会理会,就比如我这里虽然设置像素格式是yuv420p,但是我的摄像头不支持,所以在代码中会动态进行一个格式转换。整体的思路就是:注册设备 --> 尝试设置摄像头参数 --> 打开设备 --> 读取流信息 --> 找到我们所需要的视频流(一个媒体容器比如mp4,会有视频流、音频流、字母流等等)

注:如果大家不知道流的话建议去ffmpeg官方文档了解一下,对后续对流的处理有帮助,由于一两句说不明白,所以小编不在这里细说了。

int CaptureController::camera_open(const char *iurl, int w, int h, int framerate) { /* 注册设备 */ avdevice_register_all(); /* 准备参数 */ std::ostringstream size_oss; size_oss << w << "x" << h; std::ostringstream framerate_oss; framerate_oss << framerate; /* 配置参数 */ AVDictionary *options = NULL; av_dict_set(&options, "video_size", size_oss.str().c_str(), 0); av_dict_set(&options, "framerate", framerate_oss.str().c_str(), 0); av_dict_set(&options, "pixel_format", "yuv420p", 0); AVFormatContext *ifmt = NULL; int ret = avformat_open_input(&ifmt, iurl, NULL, &options); if(ret < 0) { av_log(NULL, AV_LOG_ERROR, "avformat_open_input failed: [%d]%s\n", ret, av_err2str_cpp(ret).c_str()); return -1; } cameraSource.ifmt_.reset(ifmt); /* 查找视频流 */ ret = avformat_find_stream_info(ifmt, NULL); if(ret < 0) { av_log(NULL, AV_LOG_ERROR, "avformat_find_stream_info failed: [%d]:%s\n", ret, av_err2str_cpp(ret).c_str()); return -1; } for(int i = 0; i < ifmt->nb_streams; i++) { if(ifmt->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) { cameraSource.video_stream = i; break; } } if(cameraSource.video_stream == -1) { av_log(NULL, AV_LOG_ERROR, "no find video stream\n"); return -1; } return 0; }

(2)创建解码器

        这个解码器是为了更好的兼容,摄像头可能输出两种数据:一种是RAWVIDEO(如YUYV、NV12),像这种的一般无需解码,但是可能需要格式转化,因为h264编码一般使用的是YUV420P像素格式。另一种是压缩流(如 MJPEG),像这种就必须需先解码成 YUV 帧。这里小编的摄像头是RAWVIDEO数据,所以实际运行中并没有去先进行解码。整体流程就是:找到设备解码所用的解码器id --> 创建解码器上下文 --> 填充解码器参数(这里可以直接进行复制设备视频流中的编码器参数) --> 最后就是打开解码器,如果不打开解码器后续将包(packet)送入解码器时会报错。

int CaptureController::dencoder_create() { AVFormatContext *ifmt = cameraSource.ifmt_.get(); AVCodecParameters *in_par = ifmt->streams[cameraSource.video_stream]->codecpar; if(in_par->codec_id == AV_CODEC_ID_RAWVIDEO) { return 0; } /* 找到对应解码器 */ cameraSource.denc = avcodec_find_decoder(in_par->codec_id); if(!cameraSource.denc) { av_log(NULL, AV_LOG_ERROR, "[dencoder] avcodec_find_encoder failed\n"); return -1; } /* 创建解码器上下文 */ AVCodecContext *denc_ctx = avcodec_alloc_context3(cameraSource.denc); if(!denc_ctx) { av_log(NULL, AV_LOG_ERROR, "[dencoder] avcodec_alloc_context3 failed\n"); return -1; } cameraSource.denc_ctx_.reset(denc_ctx); /* 复制一份解码器参数 */ int ret = avcodec_parameters_to_context(denc_ctx, in_par); if(ret < 0) { av_log(NULL, AV_LOG_ERROR, "[dencoder] avcodec_parameters_to_context failed: [%d]:%s\n", ret, av_err2str_cpp(ret).c_str()); return -1; } /* 打开解码器 */ ret = avcodec_open2(denc_ctx, cameraSource.denc, NULL); if(ret < 0) { av_log(NULL, AV_LOG_ERROR, "[dencoder] avcodec_open2 failed: [%d]:%s\n", ret, av_err2str_cpp(ret).c_str()); return -1; } return 0; }

接下来就是视频输出模块,也就是videoOutput结构体:

(3)打开输出文件

        这个比较简单,就是需要创建一个上下文,用于后续将h264编码后的数据流写入文件中。

int CaptureController::video_output_open(const char *ourl) { AVFormatContext *ofmt = NULL; int ret = avformat_alloc_output_context2(&ofmt, NULL, "mp4", ourl); if(ret < 0) { av_log(NULL, AV_LOG_ERROR, "avformat_alloc_output_context2 failed: [%d]%s\n", ret, av_err2str_cpp(ret).c_str()); return -1; } videoOutput.ofmt_.reset(ofmt); return 0; }

(4)编码器创建

        这个必要重要,这里需要填写的编码器参数是我们最终要得到的视频流规格,比如视频大小(如640x480)、视频像素格式(YUV420P,h264一般使用该格式)、时间戳、帧率等。这里需要格外注意AVFMT_GLOBALHEADER这个标志,像mp4、flv、mkv输出格式有这个标志位,这些容器要求将编解码器的“全局头信息”(如 SPS/PPS)单独存放在文件头部,而不是混在每一帧数据中。像 MPEG-TS容器不支持 GLOBALHEADER ,所以每一帧(尤其是关键帧)都必须自带完整头信息。其次就是preset等参数的设置,不同的场景用到的参数也就不一样,这里大家需要去h264官网文档查看手册,看看哪些参数用于什么场合,下面小编写了三种常用用途的三种参数配置,ENCODER_MODE_REALTIME_STREAM(实时推流)、ENCODER_MODE_FIXED_BITRATE(固定分辨率吧,常用于直播)、ENCODER_MODE_HIGH_QUALITY(高质量画质)。小编在这里简单说一下一些参数的用途,crf(控制视频画质,越低越好,一般在18-26之间,默认常用23),bframes(b帧是否启用,一般实时直播是禁用的,直播延迟低是第一位),preset(预设参数,编码速度与压缩效率之间的权衡配置,比如ultrafast,编码速度极快但是压缩效率最差,所以适合实时推流,低延迟直播),更多的参数需要大家自己查阅,这里就不再叙说了。

int CaptureController::encoder_create(int w, int h, int framterate, int bit_rate, encoder_mode_t mode) { if(mode >= ENCODER_MODE_END) { av_log(NULL, AV_LOG_ERROR, "encoder mode error(mode:%d < %d)\n", mode, ENCODER_MODE_END); return -1; } /* 找到编码器 */ videoOutput.enc = avcodec_find_encoder(AV_CODEC_ID_H264); if(!videoOutput.enc) { av_log(NULL, AV_LOG_ERROR, "[encoder] avcodec_find_encoder failed\n"); return -1; } /* 创建编码器上下文 */ AVCodecContext *enc_ctx = avcodec_alloc_context3(videoOutput.enc); if(!enc_ctx) { av_log(NULL, AV_LOG_ERROR, "[encoder] avcodec_alloc_context3 failed\n"); return -1; } videoOutput.enc_ctx_.reset(enc_ctx); /* 设置编码器参数 */ enc_ctx->width = w; enc_ctx->height = h; enc_ctx->framerate = (AVRational){framterate, 1}; enc_ctx->time_base = (AVRational){1, framterate}; enc_ctx->pix_fmt = AV_PIX_FMT_YUV420P; if(videoOutput.ofmt_.get()->oformat->flags & AVFMT_GLOBALHEADER) { enc_ctx->flags |= AV_CODEC_FLAG_GLOBAL_HEADER; } /* 配置参数 */ AVDictionary *options = NULL; if(mode == ENCODER_MODE_REALTIME_STREAM) { av_dict_set(&options, "preset", "ultrafast", 0); av_dict_set(&options, "tune", "zerolatency", 0); av_dict_set(&options, "crf", "23", 0); av_dict_set(&options, "bframes", "0", 0); } else if(mode == ENCODER_MODE_FIXED_BITRATE) { enc_ctx->bit_rate = bit_rate; av_dict_set(&options, "preset", "veryfast", 0); av_dict_set(&options, "tune", "zerolatency", 0); av_dict_set(&options, "bframes", "0", 0); av_dict_set(&options, "maxrate", "2M", 0); av_dict_set(&options, "bufsize", "2M", 0); } else if(mode == ENCODER_MODE_HIGH_QUALITY) { av_dict_set(&options, "preset", "slow", 0); av_dict_set(&options, "crf", "18", 0); } /* 打开编码器 */ int ret = avcodec_open2(enc_ctx, videoOutput.enc, &options); if(ret < 0) { av_log(NULL, AV_LOG_ERROR, "[encoder] avcodec_open2 failed: [%d]%s\n", ret, av_err2str_cpp(ret).c_str()); return -1; } return 0; }

(5)创建输出流

        这里也比较简单,像媒体文件中都是以流的形式存在的,所以这里我们需要创建一个视频流用于保存视频数据。这里需要注意的是将编码器的参数复制到视频流中,确保该视频能够正确解码。其次就是videoOutput.ostream->codecpar->codec_tag赋值为0,因为我们在转码时,输入流的codec_tag已经不适用于输出流了,同时ffmpeg的官方也推荐设为 0 = 让 FFmpeg 自动选择符合容器标准的 FourCC。这里给大家一个好的建议:只要重新编码(re-encode),就设 codecpar->codec_tag = 0;只有直通(stream copy)才保留原 tag。

int CaptureController::ostream_create() { /* 创建视频流 */ videoOutput.ostream = avformat_new_stream(videoOutput.ofmt_.get(), videoOutput.enc); if(!videoOutput.ostream) { av_log(NULL, AV_LOG_ERROR, "avformat_new_stream failed\n"); return -1; } /* 复制参数 */ int ret = avcodec_parameters_from_context(videoOutput.ostream->codecpar, videoOutput.enc_ctx_.get()); if(ret < 0) { av_log(NULL, AV_LOG_ERROR, "avcodec_parameters_from_context failed: [%d]%s\n", ret, av_err2str_cpp(ret).c_str()); return -1; } videoOutput.ostream->codecpar->codec_tag = 0; return 0; }

(6)初始化工具

        这里的工具是指的在编解码时中间使用到的frame和packet等,这里需要注意,frame时喂给编码器时的帧,需要为其声请数据空间,iframe不需要,他是中间用于只想数据的指针,它指向的packet的数据空间,可以节省这部分内存。32时内存对齐方式,这里参考ffmpeg推荐直接使用32。

int CaptureController::init_utils() { AVFrame *frame = av_frame_alloc(); AVFrame *iframe = av_frame_alloc(); AVPacket *pkt = av_packet_alloc(); if(!frame || !iframe || !pkt) { av_log(NULL, AV_LOG_ERROR, "frame or iframe or pkt alloc failed\n"); return -1; } utils.frame.reset(frame); utils.iframe.reset(iframe); utils.pkt_.reset(pkt); /* 为 frame 申请空间 */ frame->width = videoOutput.enc_ctx_.get()->width; frame->height = videoOutput.enc_ctx_.get()->height; frame->format = videoOutput.enc_ctx_.get()->pix_fmt; int ret = av_frame_get_buffer(frame, 32); if(ret < 0) { av_log(NULL, AV_LOG_ERROR, "av_frame_get_buffer failed: [%d]%s\n", ret, av_err2str_cpp(ret).c_str()); return -1; } return 0; }

(7)核心帧处理模块

        我们最后再讲一下核心模块(在帧处理之前也就是将包写入文件之前需要先要写一下头,头,文件结束需要写一下尾,这部分没什么可讲的,大家自行了解),核心模块小编分三部分进行处理。首先就是接收到的帧处理,判断一下是否需要解码,如果需要则喂给解码器解码然后再取出解码后的原始帧直接喂给编码器,否则需要先填充一下iframe(也就是原始数据),然后在喂给编码器;然后编码器部分会对像素格式进行一次转化,比如宽高不符、格式不符等等都会转化其符合的最终格式并保存在frame帧中;最终从编码器中接受经h264编码后的数据并写入文件,这就是一个核心的流程。

        这里重点就是对pts和dts的处理,pts:用于控制什么时候显示该帧,dts用于解码的先后顺序(这是由于B帧不仅需要参考I帧还需要参考P帧,所以压缩式会把P帧压缩在前,比如正常的顺序I、B、B、P,压缩时就会I、P、B、B)。首先就是保证 dts严格单调递增(不回退),这是 MP4/FLV 等容器格式的硬性要求。比如dts = [0, 30, 30, 60],两个相同的dts会导致播放器卡死、丢帧、报错等(这里需要注意的是pts可以乱序(B 帧场景),但 dts绝对不能回退或相等!)。所以再pkt_write_file函数设置pts时不仅需要保证dts单调递增且需要同步pts的偏移值(pts - dts 的差值决定了解码后多久显示,这是播放同步的关键,当然我这里用了比较简单的同步)。

int CaptureController::pkt_write_file() { AVPacket *pkt = utils.pkt_.get(); av_packet_unref(pkt); while(1) { int ret = avcodec_receive_packet(videoOutput.enc_ctx_.get(), pkt); if(ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) break; else if(ret < 0) { av_log(NULL, AV_LOG_ERROR, "avcodec_receive_packet falied: [%d]%s\n", ret, av_err2str_cpp(ret).c_str()); return -1; } /* 转化时间基、设置pts */ av_packet_rescale_ts(pkt, videoOutput.enc_ctx_.get()->time_base, videoOutput.ostream->time_base); if(pkt->dts != AV_NOPTS_VALUE) { if(pkt->dts <= get_lastdts()) { /* 同时修正 pts 和 dts 的偏移量,保持相对关系 */ int64_t offset = get_lastdts() + 1 - pkt->dts; pkt->dts += offset; if(pkt->pts != AV_NOPTS_VALUE) { pkt->pts += offset; } } } set_lastdts(pkt->dts); /* 写入文件 */ ret = av_write_frame(videoOutput.ofmt_.get(), pkt); if(ret < 0) { av_log(NULL, AV_LOG_ERROR, "av_write_frame failed: [%d]%s\n", ret, av_err2str_cpp(ret).c_str()); return -1; } av_packet_unref(pkt); } return 0; } int CaptureController::encoder_and_write_file(int64_t pts) { /* 转化编码器需要的帧格式 */ swscale_format(); /* 设置pts(需要单增) */ if(pts <= get_lastpts()) pts = get_lastpts() + 1; set_lastpts(pts); utils.frame.get()->pts = pts; /* 发送帧到编码器 */ int ret = avcodec_send_frame(videoOutput.enc_ctx_.get(), utils.frame.get()); if(ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) { return 0; } else if(ret < 0) { av_log(NULL, AV_LOG_ERROR, "avcodec_send_frame failed: [%d]:%s", ret, av_err2str_cpp(ret).c_str()); return -1; } /* 接受编码器处理后的包并写入文件 */ ret = pkt_write_file(); if(ret == -1) { av_log(NULL, AV_LOG_ERROR, "pkt_write_file failed\n"); return -1; } return 0; } int CaptureController::process_frame(int64_t start_time, int64_t captrue_time) { AVFrame *iframe = utils.iframe.get(); AVPacket *pkt = utils.pkt_.get(); AVCodecParameters *in_par = cameraSource.ifmt_.get()->streams[cameraSource.video_stream]->codecpar; if(cameraSource.denc_ctx_.get()) { int ret = avcodec_send_packet(cameraSource.denc_ctx_.get(), pkt); if(ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) { return 0; } else if(ret < 0) { av_log(NULL, AV_LOG_ERROR, "avcodec_send_packet failed: [%d]%s\n", ret, av_err2str_cpp(ret).c_str()); return -1; } while(ret >= 0) { /* 接受帧 */ ret = avcodec_receive_frame(cameraSource.denc_ctx_.get(), iframe); if(ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) continue; else if(ret < 0) { av_log(NULL, AV_LOG_ERROR, "avcodec_receive_frame failed: [%d]%s\n", ret, av_err2str_cpp(ret).c_str()); break; } /* 初始化格式转化工具 */ if(init_sws(true) != 0) { return -1; } /* 设置 pts 并处理写入帧 */ int64_t pts = av_rescale_q(captrue_time - start_time, AV_TIME_BASE_Q, videoOutput.enc_ctx_.get()->time_base); if(encoder_and_write_file(pts) == -1) return -1; } } else { /* 填充原始数据 */ av_image_fill_arrays( const_cast<uint8_t **>(iframe->data), iframe->linesize, const_cast<const uint8_t *>(pkt->data), (AVPixelFormat)in_par->format, in_par->width, in_par->height, 1 ); iframe->width = in_par->width; iframe->height = in_par->height; iframe->format = in_par->format; /* 初始化格式转化工具 */ if(init_sws(false) != 0) { return -1; } /* 设置 pts 并处理写入帧 */ int64_t pts = av_rescale_q(captrue_time - start_time, AV_TIME_BASE_Q, videoOutput.enc_ctx_.get()->time_base); if(encoder_and_write_file(pts) == -1) return -1; } av_packet_unref(pkt); return 1; }

四、代码展览

        核心部分小编已经分享完了,最后再展示完整代码,感谢大家的感官,也希望大家也有所收获,大家在终端执行下面命令即可编译(注意先apt install ffmpeg的库),期待下次空闲时间做一期上面说的物体检测vidora。

g++ ffmpeg_camera.cpp -o capture_video `pkg-config --cflags --libs libavformat libavcodec libavutil libswscale libavdevice` -lpthread
 #include <iostream> #include <memory> #include <sstream> #include <atomic> extern "C" { #include <signal.h> #include "libavformat/avformat.h" #include "libavcodec/avcodec.h" #include "libswscale/swscale.h" #include "libavutil/imgutils.h" #include "libavutil/time.h" #include "libavdevice/avdevice.h" } #define VIDEO_WIDTH 640 #define VIDEO_HEIGHT 480 #define FRAMERATE 30 #define BITRATE 2000000 static std::string av_err2str_cpp(int errnum) { char buf[AV_ERROR_MAX_STRING_SIZE]; return av_strerror(errnum, buf, AV_ERROR_MAX_STRING_SIZE) ? std::string("Unknown error") : std::string(buf); } /* 编解码器智能指针 */ struct AVCodecContextDelete { void operator()(AVCodecContext *ctx) const { if(ctx) avcodec_free_context(&ctx); } }; using AVCodecContextPtr = std::unique_ptr<AVCodecContext, AVCodecContextDelete>; struct AVFrameDelete { void operator()(AVFrame *frame) const { if(frame) av_frame_free(&frame); } }; using AVFramePtr = std::unique_ptr<AVFrame, AVFrameDelete>; class CaptureController { public: enum encoder_mode_t{ ENCODER_MODE_REALTIME_STREAM, ENCODER_MODE_FIXED_BITRATE, ENCODER_MODE_HIGH_QUALITY, ENCODER_MODE_END }; ~CaptureController(); int camera_open(const char *iurl, int w, int h, int framerate); int dencoder_create(); int video_output_open(const char *ourl); int encoder_create(int w, int h, int framterate, int bit_rate, encoder_mode_t mode); int ostream_create(); int init_utils(); int process_frame(int64_t start_time, int64_t captrue_time); int flush_frame(); int write_header(const char *ourl) { if(!(videoOutput.ofmt_.get()->oformat->flags & AVFMT_NOFILE)) { int ret = avio_open(&videoOutput.ofmt_.get()->pb, ourl, AVIO_FLAG_WRITE); if(ret < 0) { av_log(NULL, AV_LOG_ERROR, "avio_open failed: [%d]%s\n", ret, av_err2str_cpp(ret).c_str()); return ret; } } return avformat_write_header(videoOutput.ofmt_.get(), NULL); } int write_trailer() { return av_write_trailer(videoOutput.ofmt_.get()); } int read_frame() { int ret = av_read_frame(cameraSource.ifmt_.get(), utils.pkt_.get()); if(ret < 0) { av_log(NULL, AV_LOG_ERROR, "av_read_frame failed: [%d]%s\n", ret, av_err2str_cpp(ret).c_str()); return -1; } if(utils.pkt_.get()->stream_index != cameraSource.video_stream) { av_packet_unref(utils.pkt_.get()); return 0; } return 1; } void set_lastpts(int64_t pts) { utils.last_pts = pts; } int64_t get_lastpts() { return utils.last_pts; } void set_lastdts(int64_t dts) { utils.last_dts = dts; } int64_t get_lastdts() { return utils.last_dts; } private: int init_sws(bool has_denc); void swscale_format(); int encoder_and_write_file(int64_t pts); int pkt_write_file(); struct { std::unique_ptr<AVFormatContext, void (*)(AVFormatContext *)> ifmt_ { /* 输入上下文 */ nullptr, [](AVFormatContext *ifmt) { if(ifmt) avformat_close_input(&ifmt); } }; AVCodecContextPtr denc_ctx_; /* 解码器 */ AVCodec *denc = NULL; int video_stream = -1; /* 视频流 */ } cameraSource; struct { std::unique_ptr<AVFormatContext, void (*)(AVFormatContext *)> ofmt_ { /* 输出上下文 */ nullptr, [](AVFormatContext *ofmt) { if(ofmt) avformat_free_context(ofmt); } }; AVCodec *enc = NULL; AVCodecContextPtr enc_ctx_; /* 编码器 */ AVStream *ostream = NULL; /* 输出视频流 */ } videoOutput; struct { AVFramePtr iframe; /* 中间帧,只负责指向原始数据 */ AVFramePtr frame; /* 实际帧 */ std::unique_ptr<AVPacket, void (*)(AVPacket *)> pkt_ { nullptr, [](AVPacket *pkt){ if(pkt) av_packet_free(&pkt); } }; std::unique_ptr<struct SwsContext, void (*)(struct SwsContext *)> sws_ctx_ { nullptr, [](struct SwsContext *sws_ctx) { if(sws_ctx) sws_freeContext(sws_ctx); } }; int64_t last_pts = -1, last_dts = -1; bool sws_initd = false; } utils; }; CaptureController::~CaptureController() { if(videoOutput.ofmt_.get()) { if(videoOutput.ofmt_.get()->oformat->flags & AVFMT_NOFILE) { avio_closep(&videoOutput.ofmt_.get()->pb); } } } int CaptureController::camera_open(const char *iurl, int w, int h, int framerate) { /* 注册设备 */ avdevice_register_all(); /* 准备参数 */ std::ostringstream size_oss; size_oss << w << "x" << h; std::ostringstream framerate_oss; framerate_oss << framerate; /* 配置参数 */ AVDictionary *options = NULL; av_dict_set(&options, "video_size", size_oss.str().c_str(), 0); av_dict_set(&options, "framerate", framerate_oss.str().c_str(), 0); av_dict_set(&options, "pixel_format", "yuv420p", 0); AVFormatContext *ifmt = NULL; int ret = avformat_open_input(&ifmt, iurl, NULL, &options); if(ret < 0) { av_log(NULL, AV_LOG_ERROR, "avformat_open_input failed: [%d]%s\n", ret, av_err2str_cpp(ret).c_str()); return -1; } cameraSource.ifmt_.reset(ifmt); /* 查找视频流 */ ret = avformat_find_stream_info(ifmt, NULL); if(ret < 0) { av_log(NULL, AV_LOG_ERROR, "avformat_find_stream_info failed: [%d]:%s\n", ret, av_err2str_cpp(ret).c_str()); return -1; } for(int i = 0; i < ifmt->nb_streams; i++) { if(ifmt->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) { cameraSource.video_stream = i; break; } } if(cameraSource.video_stream == -1) { av_log(NULL, AV_LOG_ERROR, "no find video stream\n"); return -1; } return 0; } int CaptureController::dencoder_create() { AVFormatContext *ifmt = cameraSource.ifmt_.get(); AVCodecParameters *in_par = ifmt->streams[cameraSource.video_stream]->codecpar; if(in_par->codec_id == AV_CODEC_ID_RAWVIDEO) { return 0; } /* 找到对应解码器 */ cameraSource.denc = avcodec_find_decoder(in_par->codec_id); if(!cameraSource.denc) { av_log(NULL, AV_LOG_ERROR, "[dencoder] avcodec_find_encoder failed\n"); return -1; } /* 创建解码器上下文 */ AVCodecContext *denc_ctx = avcodec_alloc_context3(cameraSource.denc); if(!denc_ctx) { av_log(NULL, AV_LOG_ERROR, "[dencoder] avcodec_alloc_context3 failed\n"); return -1; } cameraSource.denc_ctx_.reset(denc_ctx); /* 复制一份解码器参数 */ int ret = avcodec_parameters_to_context(denc_ctx, in_par); if(ret < 0) { av_log(NULL, AV_LOG_ERROR, "[dencoder] avcodec_parameters_to_context failed: [%d]:%s\n", ret, av_err2str_cpp(ret).c_str()); return -1; } /* 打开解码器 */ ret = avcodec_open2(denc_ctx, cameraSource.denc, NULL); if(ret < 0) { av_log(NULL, AV_LOG_ERROR, "[dencoder] avcodec_open2 failed: [%d]:%s\n", ret, av_err2str_cpp(ret).c_str()); return -1; } return 0; } int CaptureController::video_output_open(const char *ourl) { AVFormatContext *ofmt = NULL; int ret = avformat_alloc_output_context2(&ofmt, NULL, "mp4", ourl); if(ret < 0) { av_log(NULL, AV_LOG_ERROR, "avformat_alloc_output_context2 failed: [%d]%s\n", ret, av_err2str_cpp(ret).c_str()); return -1; } videoOutput.ofmt_.reset(ofmt); return 0; } int CaptureController::encoder_create(int w, int h, int framterate, int bit_rate, encoder_mode_t mode) { if(mode >= ENCODER_MODE_END) { av_log(NULL, AV_LOG_ERROR, "encoder mode error(mode:%d < %d)\n", mode, ENCODER_MODE_END); return -1; } /* 找到编码器 */ videoOutput.enc = avcodec_find_encoder(AV_CODEC_ID_H264); if(!videoOutput.enc) { av_log(NULL, AV_LOG_ERROR, "[encoder] avcodec_find_encoder failed\n"); return -1; } /* 创建编码器上下文 */ AVCodecContext *enc_ctx = avcodec_alloc_context3(videoOutput.enc); if(!enc_ctx) { av_log(NULL, AV_LOG_ERROR, "[encoder] avcodec_alloc_context3 failed\n"); return -1; } videoOutput.enc_ctx_.reset(enc_ctx); /* 设置编码器参数 */ enc_ctx->width = w; enc_ctx->height = h; enc_ctx->framerate = (AVRational){framterate, 1}; enc_ctx->time_base = (AVRational){1, framterate}; enc_ctx->pix_fmt = AV_PIX_FMT_YUV420P; if(videoOutput.ofmt_.get()->oformat->flags & AVFMT_GLOBALHEADER) { enc_ctx->flags |= AV_CODEC_FLAG_GLOBAL_HEADER; } /* 配置参数 */ AVDictionary *options = NULL; if(mode == ENCODER_MODE_REALTIME_STREAM) { av_dict_set(&options, "preset", "ultrafast", 0); av_dict_set(&options, "tune", "zerolatency", 0); av_dict_set(&options, "crf", "23", 0); av_dict_set(&options, "bframes", "0", 0); } else if(mode == ENCODER_MODE_FIXED_BITRATE) { enc_ctx->bit_rate = bit_rate; av_dict_set(&options, "preset", "veryfast", 0); av_dict_set(&options, "tune", "zerolatency", 0); av_dict_set(&options, "bframes", "0", 0); av_dict_set(&options, "maxrate", "2M", 0); av_dict_set(&options, "bufsize", "2M", 0); } else if(mode == ENCODER_MODE_HIGH_QUALITY) { av_dict_set(&options, "preset", "slow", 0); av_dict_set(&options, "crf", "18", 0); } /* 打开编码器 */ int ret = avcodec_open2(enc_ctx, videoOutput.enc, &options); if(ret < 0) { av_log(NULL, AV_LOG_ERROR, "[encoder] avcodec_open2 failed: [%d]%s\n", ret, av_err2str_cpp(ret).c_str()); return -1; } return 0; } int CaptureController::ostream_create() { /* 创建视频流 */ videoOutput.ostream = avformat_new_stream(videoOutput.ofmt_.get(), videoOutput.enc); if(!videoOutput.ostream) { av_log(NULL, AV_LOG_ERROR, "avformat_new_stream failed\n"); return -1; } /* 复制参数 */ int ret = avcodec_parameters_from_context(videoOutput.ostream->codecpar, videoOutput.enc_ctx_.get()); if(ret < 0) { av_log(NULL, AV_LOG_ERROR, "avcodec_parameters_from_context failed: [%d]%s\n", ret, av_err2str_cpp(ret).c_str()); return -1; } videoOutput.ostream->codecpar->codec_tag = 0; return 0; } int CaptureController::init_utils() { AVFrame *frame = av_frame_alloc(); AVFrame *iframe = av_frame_alloc(); AVPacket *pkt = av_packet_alloc(); if(!frame || !iframe || !pkt) { av_log(NULL, AV_LOG_ERROR, "frame or iframe or pkt alloc failed\n"); return -1; } utils.frame.reset(frame); utils.iframe.reset(iframe); utils.pkt_.reset(pkt); /* 为 frame 申请空间 */ frame->width = videoOutput.enc_ctx_.get()->width; frame->height = videoOutput.enc_ctx_.get()->height; frame->format = videoOutput.enc_ctx_.get()->pix_fmt; int ret = av_frame_get_buffer(frame, 32); if(ret < 0) { av_log(NULL, AV_LOG_ERROR, "av_frame_get_buffer failed: [%d]%s\n", ret, av_err2str_cpp(ret).c_str()); return -1; } return 0; } int CaptureController::init_sws(bool has_denc) { if(!utils.sws_initd) { utils.sws_initd = true; int src_w, src_h; AVPixelFormat src_format; if(has_denc) { src_w = utils.iframe.get()->width; src_h = utils.iframe.get()->height; src_format = (AVPixelFormat)utils.iframe.get()->format; } else { AVCodecParameters *in_par = cameraSource.ifmt_.get()->streams[cameraSource.video_stream]->codecpar; src_w = in_par->width; src_h = in_par->height; src_format = (AVPixelFormat)in_par->format; } AVCodecContext *enc_ctx = videoOutput.enc_ctx_.get(); if(src_w != enc_ctx->width || src_h != enc_ctx->height || src_format != enc_ctx->pix_fmt) { struct SwsContext *sws_ctx = sws_getContext( src_w, src_h, src_format, enc_ctx->width, enc_ctx->height, enc_ctx->pix_fmt, SWS_BILINEAR, NULL, NULL, NULL ); if(!sws_ctx) { av_log(NULL, AV_LOG_ERROR, "sws_getContext failed\n"); return -1; } utils.sws_ctx_.reset(sws_ctx); } } return 0; } void CaptureController::swscale_format() { AVFrame *frame = utils.frame.get(); AVFrame *iframe = utils.iframe.get(); if(utils.sws_ctx_.get()) { sws_scale(utils.sws_ctx_.get(), const_cast<const uint8_t *const *>(iframe->data), iframe->linesize, 0, iframe->height, frame->data, frame->linesize ); } else { av_image_copy( const_cast<uint8_t **>(frame->data), frame->linesize, const_cast<const uint8_t **>(iframe->data), iframe->linesize, videoOutput.enc_ctx_.get()->pix_fmt, videoOutput.enc_ctx_.get()->width, videoOutput.enc_ctx_.get()->height ); } } int CaptureController::pkt_write_file() { AVPacket *pkt = utils.pkt_.get(); av_packet_unref(pkt); while(1) { int ret = avcodec_receive_packet(videoOutput.enc_ctx_.get(), pkt); if(ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) break; else if(ret < 0) { av_log(NULL, AV_LOG_ERROR, "avcodec_receive_packet falied: [%d]%s\n", ret, av_err2str_cpp(ret).c_str()); return -1; } /* 转化时间基、设置pts */ av_packet_rescale_ts(pkt, videoOutput.enc_ctx_.get()->time_base, videoOutput.ostream->time_base); if(pkt->dts != AV_NOPTS_VALUE) { if(pkt->dts <= get_lastdts()) { /* 同时修正 pts 和 dts 的偏移量,保持相对关系 */ int64_t offset = get_lastdts() + 1 - pkt->dts; pkt->dts += offset; if(pkt->pts != AV_NOPTS_VALUE) { pkt->pts += offset; } } } set_lastdts(pkt->dts); /* 写入文件 */ ret = av_write_frame(videoOutput.ofmt_.get(), pkt); if(ret < 0) { av_log(NULL, AV_LOG_ERROR, "av_write_frame failed: [%d]%s\n", ret, av_err2str_cpp(ret).c_str()); return -1; } av_packet_unref(pkt); } return 0; } int CaptureController::encoder_and_write_file(int64_t pts) { /* 转化编码器需要的帧格式 */ swscale_format(); /* 设置pts(需要单增) */ if(pts <= get_lastpts()) pts = get_lastpts() + 1; set_lastpts(pts); utils.frame.get()->pts = pts; /* 发送帧到编码器 */ int ret = avcodec_send_frame(videoOutput.enc_ctx_.get(), utils.frame.get()); if(ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) { return 0; } else if(ret < 0) { av_log(NULL, AV_LOG_ERROR, "avcodec_send_frame failed: [%d]:%s", ret, av_err2str_cpp(ret).c_str()); return -1; } /* 接受编码器处理后的包并写入文件 */ ret = pkt_write_file(); if(ret == -1) { av_log(NULL, AV_LOG_ERROR, "pkt_write_file failed\n"); return -1; } return 0; } int CaptureController::process_frame(int64_t start_time, int64_t captrue_time) { AVFrame *iframe = utils.iframe.get(); AVPacket *pkt = utils.pkt_.get(); AVCodecParameters *in_par = cameraSource.ifmt_.get()->streams[cameraSource.video_stream]->codecpar; if(cameraSource.denc_ctx_.get()) { int ret = avcodec_send_packet(cameraSource.denc_ctx_.get(), pkt); if(ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) { return 0; } else if(ret < 0) { av_log(NULL, AV_LOG_ERROR, "avcodec_send_packet failed: [%d]%s\n", ret, av_err2str_cpp(ret).c_str()); return -1; } while(ret >= 0) { /* 接受帧 */ ret = avcodec_receive_frame(cameraSource.denc_ctx_.get(), iframe); if(ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) continue; else if(ret < 0) { av_log(NULL, AV_LOG_ERROR, "avcodec_receive_frame failed: [%d]%s\n", ret, av_err2str_cpp(ret).c_str()); break; } /* 初始化格式转化工具 */ if(init_sws(true) != 0) { return -1; } /* 设置 pts 并处理写入帧 */ int64_t pts = av_rescale_q(captrue_time - start_time, AV_TIME_BASE_Q, videoOutput.enc_ctx_.get()->time_base); if(encoder_and_write_file(pts) == -1) return -1; } } else { /* 填充原始数据 */ av_image_fill_arrays( const_cast<uint8_t **>(iframe->data), iframe->linesize, const_cast<const uint8_t *>(pkt->data), (AVPixelFormat)in_par->format, in_par->width, in_par->height, 1 ); iframe->width = in_par->width; iframe->height = in_par->height; iframe->format = in_par->format; /* 初始化格式转化工具 */ if(init_sws(false) != 0) { return -1; } /* 设置 pts 并处理写入帧 */ int64_t pts = av_rescale_q(captrue_time - start_time, AV_TIME_BASE_Q, videoOutput.enc_ctx_.get()->time_base); if(encoder_and_write_file(pts) == -1) return -1; } av_packet_unref(pkt); return 1; } int CaptureController::flush_frame() { /* 发送 NULL 帧结束编码 */ avcodec_send_frame(videoOutput.enc_ctx_.get(), NULL); /* 接受编码器处理后的包并写入文件 */ int ret = pkt_write_file(); if(ret == -1) { av_log(NULL, AV_LOG_ERROR, "pkt_write_file failed\n"); return -1; } return 0; }; static std::atomic<bool> g_running; void handle_sigint(int sig) { g_running.store(false); } int main(int argc, char *argv[]) { if(argc != 3) { printf("Usage: %s <input device> <output filename>\n", argv[0]); return -1; } /* 注册信号退出 */ signal(SIGINT, handle_sigint); g_running.store(true); const char *iurl = argv[1]; const char *ourl = argv[2]; CaptureController capture; /* 打开输入设备 */ int ret = capture.camera_open(iurl, VIDEO_WIDTH, VIDEO_HEIGHT, FRAMERATE); if(ret < 0) { perror("camera_open failed\n"); return -1; } /* 创建解码器 */ ret = capture.dencoder_create(); if(ret < 0) { perror("dencoder_create failed\n"); return -1; } /* 打开输出文件 */ ret = capture.video_output_open(ourl); if(ret < 0) { perror("video_output_open failed\n"); return -1; } /* 创建编码器 */ ret = capture.encoder_create(VIDEO_WIDTH, VIDEO_HEIGHT, FRAMERATE, BITRATE, CaptureController::ENCODER_MODE_HIGH_QUALITY); if(ret < 0) { perror("encoder_create failed\n"); return -1; } /* 创建输出流 */ ret = capture.ostream_create(); if(ret < 0) { perror("ostream_create failed\n"); return -1; } /* 初始化录取阶段需要的中间工具 */ ret = capture.init_utils(); if(ret < 0) { perror("init_utils failed\n"); return -1; } /* 写头 */ ret = capture.write_header(ourl); if(ret < 0) { av_log(NULL, AV_LOG_ERROR, "write_header failed: [%d]%s\n", ret, av_err2str_cpp(ret).c_str()); return -1; } int64_t start_time = av_gettime_relative(); int64_t capture_time = -1; while(g_running.load(std::memory_order_acquire)) { ret = capture.read_frame(); if(ret == -1) break; if(ret == 0) continue; capture_time = av_gettime_relative(); ret = capture.process_frame(start_time, capture_time); if(ret == -1) break; if(ret == 0) continue; } /* 刷新 flash,写入最后几帧 */ capture.flush_frame(); /* 写尾 */ ret = capture.write_trailer(); if(ret < 0) { av_log(NULL , AV_LOG_ERROR, "write_trailer falied: [%d]%s\n", ret, av_err2str_cpp(ret).c_str()); return -1; } return 0; }

运行成功的界面以及使用ffprobe查看mp4信息,播放就不了笔者不太好看(大家记得先把虚拟机camera连接成功,直接使用pc端的摄像头)。

Read more

Spring Boot 视图层与模板引擎

Spring Boot 视图层与模板引擎

Spring Boot 视图层与模板引擎 19.1 学习目标与重点提示 学习目标:掌握Spring Boot视图层与模板引擎的核心概念与使用方法,包括Spring Boot视图层的基本方法、Spring Boot与Thymeleaf的集成、Spring Boot与Freemarker的集成、Spring Boot与Velocity的集成、Spring Boot的静态资源管理、Spring Boot的实际应用场景,学会在实际开发中处理视图层问题。 重点:Spring Boot视图层的基本方法、Spring Boot与Thymeleaf的集成、Spring Boot与Freemarker的集成、Spring Boot与Velocity的集成、Spring Boot的静态资源管理、Spring Boot的实际应用场景。 19.2 Spring Boot视图层概述 Spring Boot视图层是指使用Spring Boot进行Web应用开发的方法。 19.2.1 视图层的定义 定义:视图层是指使用Spring Boot进行Web应用开发的方法。 作用:

By Ne0inhk

Spring Boot整合RocketMQ避坑指南:当Tag遇上selectorExpression的那些坑

Spring Boot整合RocketMQ避坑指南:当Tag遇上selectorExpression的那些坑 在微服务架构和异步通信成为主流的今天,消息队列作为系统解耦、流量削峰的关键组件,其重要性不言而喻。RocketMQ凭借其高吞吐、高可用和丰富的消息过滤机制,在众多项目中脱颖而出。对于已经熟悉其基础用法的开发者而言,从“能用”到“用好”的跨越,往往就藏在那些看似不起眼的配置细节里。特别是当消息过滤与Tag机制结合时,一个配置参数的误解,就可能导致消息的“神秘失踪”,让开发者在排查问题时耗费大量精力。这篇文章,我们就来深入聊聊Spring Boot项目中,使用RocketMQTemplate和@RocketMQMessageListener时,围绕Tag和selectorExpression的那些典型“坑点”,并通过实际的代码实验,为你提供一套清晰的避坑地图和排查工具。 1. 理解Tag与selectorExpression:不只是简单的字符串匹配 在RocketMQ的世界里,Topic是消息的一级分类,而Tag则是二级分类,你可以把它理解为Topic下的一个子集。这种设计

By Ne0inhk
Clawdbot(Moltbot)源码部署全实测:从环境搭建到 WebChat 验证,避坑指南收好

Clawdbot(Moltbot)源码部署全实测:从环境搭建到 WebChat 验证,避坑指南收好

一、为啥折腾 Clawdbot? 最近刷技术圈总刷到 Clawdbot(后来也叫 Moltbot),说是能搭私人 AI 助手,支持 WhatsApp、Telegram 这些常用通道,还能跑在自己设备上,不用依赖第三方服务 —— 想着拉下来测试一下功能,顺便研究一下其源码的实现。 于是拉上 GitHub 仓库https://github.com/openclaw/openclaw,打算从源码部署试试,过程里踩了不少坑,干脆整理成记录,给同样想折腾的朋友避避坑。 二、源码部署前的准备:Windows 环境优先选 WSL2 一开始想直接用 Windows CMD 部署,结果装依赖时各种报错,查仓库文档才发现 Windows 推荐用 WSL2(Ubuntu/Debian 镜像就行),后续操作全在 WSL2 里完成: 1.

By Ne0inhk
RUST:异步代码的测试与调试艺术

RUST:异步代码的测试与调试艺术

RUST:异步代码的测试与调试艺术 一、异步测试的本质与难点 1.1 异步测试与同步测试的区别 💡在Rust同步编程中,测试通常是顺序执行的,每个测试函数会阻塞线程直到完成,结果是确定的。而异步测试的结果可能受到任务调度、网络延迟、数据库连接等因素的影响,时序性和状态管理更加复杂。 同步测试示例: #[cfg(test)]modtests{#[test]fntest_add(){assert_eq!(1+1,2);}} 异步测试示例(使用Tokio测试宏): #[cfg(test)]modtests{usetokio::time::sleep;usestd::time::Duration;#[tokio::test]asyncfntest_async_add(){sleep(Duration::from_millis(100)).await;assert_

By Ne0inhk