跳到主要内容
极客日志极客日志面向AI+效率的开发者社区
首页博客GitHub 精选镜像工具UI配色美学隐私政策关于联系
搜索内容 / 工具 / 仓库 / 镜像...⌘K搜索
注册
博客列表
C++AI算法

FFmpeg/OpenCV+C++ 实现直播拉流与推流及视频帧处理

综述由AI生成介绍使用 FFmpeg 和 OpenCV 结合 C++ 进行直播流拉取、解码、图像处理及推流的完整方案。涵盖封装解封装原理、编解码基础、硬件加速概念及 FFmpeg 八大库功能。核心实践包括从 RTMP 拉流,解码为 AVFrame,转换为 OpenCV Mat 进行自定义处理(如 AI 识别),再编码回 H.264 并推送到目标地址。提供了详细的格式转换代码、工程实现流程及常用工具类封装示例,解决了音视频同步、时间戳处理及内存管理问题。

独立开发者发布于 2026/3/27更新于 2026/5/2933 浏览
FFmpeg/OpenCV+C++ 实现直播拉流与推流及视频帧处理

基础知识

封装和解封装

原始图像序列(frame₀, frame₁, frame₂, ..., frameₙ) ↓ [H.264 编码器] ↓ H.264 视频编码流(包含 I 帧、P 帧、B 帧) ↓ [MP4 封装器] ↓ MP4 文件(movie.mp4) ├── 视频流(H.264 编码) ├── 音频流(可选,如 AAC 编码) └── 元数据(时间戳、分辨率等)

编码与解码

MP4 文件(movie.mp4) ↓ [MP4 解封装器] ↓ 分离出的流 ├── H.264 视频流(包含 I 帧、P 帧、B 帧) └── 音频流(如果有) ↓ [H.264 解码器] ↓ 重建图像序列(recon₀, recon₁, recon₂, ..., reconₙ)

硬件加速编解码

工具介绍

FFmpeg '八大金刚'核心开发库。

  1. AVUtil 核心工具库,用于辅助实现可移植的多媒体编程。包含安全的可移植的字符串函数,随机数生成器,附加的数学函数,密码学和多媒体相关的功能函数。
  2. AVFormat 协议及容器封装库。该库封装了协议层(Protocol 协议,包括文件的数据来源,I/O 操作方式等)和媒体格式容器层(Muxer/Demuxer 复用/解复用,媒体文件的封装和解封装方式)。AVFormat 库支持多种输入和输出协议(FILE、HTTP、UDP、RTMP 等),以及多种媒体容器格式(MP4,RMVB 等)。
  3. AVCodec 编解码库,封装了编解码层(Codec),提供了一个通用的编码/解码框架,包含了用于音频、视频和字幕流的多个编码器和解码器。也可以将其他的第三方的编解码器以插件形式加入(如 x264,x265 等)。
  4. AVFilter 滤镜库,提供了一个通用的可对音频/视频进行过滤处理的框架,其中包含过滤器,数据源和接收器的概念。
  5. Postproc 后处理库,包括对于音频/视频文件进行后期处理的常用操作例如隔行滤波,去噪滤波、锐化滤波等,可以用于增强视频的清晰度、减少噪点和伪影等。常与 AVFilter 库一起使用,新版本已经移除。
  6. AVDevice 设备库,提供了一个通用框架,用于对许多常见的多媒体 I/O 设备进行抓取和渲染。例如:需要操作:【/dev/videoX】
  7. SWresample 音频处理库,用于处理音频,可以执行音频的重采样、重矩阵化和样本格式转换等操作。
  8. SWscale 图像转换库,用于处理图像,可以执行图像缩放,色彩空间和像素格式转换等操作。例如:需要将 FFmpeg 中的 YUV 格式转换为 OpenCV 中的 BGR 格式。

工程项目实践

工程目标:从一个 RTMP 输入流(如 rtmp://.../live/456)拉取视频流,使用 FFmpeg 解码后,再通过 OpenCV 进行可能的图像处理(目前注释掉了),然后重新编码为 H.264 视频流,并推送到另一个 RTMP 地址(如 rtmp://.../live/dj/1ZNBJ7C00C009X)。

功能分解说明

  1. 输入源:使用 FFmpeg 的 libavformat / libavcodec 打开并读取一个 (也可以是本地文件或其它协议)。自动探测流信息,找到视频流()。使用对应的解码器(如 H.264)进行解码,得到原始帧()。

RTMP 流
AVMEDIA_TYPE_VIDEO
AVFrame
  • 图像处理(可选):将解码后的 AVFrame 转换为 OpenCV 的 cv::Mat(BGR 格式)。注释中提到可以在此处添加:叠加文字/Logo、目标检测(如 YOLO)、图像增强等。当前未启用任何处理逻辑。

  • 重新编码:将处理后的 cv::Mat(BGR)转换为 YUV420P 格式的 AVFrame(通过 CVMatToAVFrame 函数)。使用 H.264 编码器(AV_CODEC_ID_H264)对每一帧进行编码。设置了编码参数:码率:400 kbps(50 * 1024 * 8 实际是 400,000 bps),GOP = 30(每 30 帧一个关键帧),分辨率、帧率继承自输入流。

  • 输出推流:使用 FLV 封装格式(RTMP 标准封装)。通过 avformat_write_header 和 av_interleaved_write_frame 将编码后的 H.264 数据推送到目标 RTMP 地址。时间戳(PTS/DTS)经过正确重缩放(av_rescale_q),保证播放同步。

  • 2.1 格式转换

    2.1.1 AVFrame 转 Mat

    Mat 是 OpenCV 的图像格式,颜色空间为 BGR,对应 FFmpeg 格式为 AV_PIX_FMT_BGR24。AVFrame 一般为 YUV420P,以此格式为例。这个通过 FFmpeg 的格式转换函数就可以解决。转换代码如下:

    cv::Mat AVFrameToCVMat(AVFrame *yuv420Frame) {
        // 得到 AVFrame 信息
        int srcW = yuv420Frame->width;
        int srcH = yuv420Frame->height;
        SwsContext *swsCtx = sws_getContext(srcW, srcH, (AVPixelFormat)yuv420Frame->format, srcW, srcH, (AVPixelFormat)AV_PIX_FMT_BGR24, SWS_BICUBIC, NULL, NULL, NULL);
        // 生成 Mat 对象
        cv::Mat mat;
        mat.create(cv::Size(srcW, srcH), CV_8UC3);
        // 格式转换,直接填充 Mat 的数据 data
        AVFrame *bgr24Frame = av_frame_alloc();
        av_image_fill_arrays(bgr24Frame->data, bgr24Frame->linesize, (uint8_t *)mat.data, (AVPixelFormat)AV_PIX_FMT_BGR24, srcW, srcH, 1);
        sws_scale(swsCtx, (const uint8_t* const*)yuv420Frame->data, yuv420Frame->linesize, 0, srcH, bgr24Frame->data, bgr24Frame->linesize);
        // 释放
        av_frame_free(bgr24Frame);
        sws_freeContext(swsCtx);
        return mat;
    }
    
    2.1.2 Mat 转 AVFrame

    借鉴上步,首先也是想利用 FFmpeg 的转换函数进行转换,但是总有问题,没有定位到具体原因。不过理解原理后,也可以自己处理数据。基本思路是将 Mat 转换为 YUV420 格式,将 Y、U、V 分量分别填充到对应的 AVFrame 里面就可以了。

    AVFrame *CVMatToAVFrame(cv::Mat &inMat) {
        // 得到 Mat 信息
        AVPixelFormat dstFormat = AV_PIX_FMT_YUV420P;
        int width = inMat.cols;
        int height = inMat.rows;
        // 创建 AVFrame 填充参数 注:调用者释放该 frame
        AVFrame *frame = av_frame_alloc();
        frame->width = width;
        frame->height = height;
        frame->format = dstFormat;
        // 初始化 AVFrame 内部空间
        int ret = av_frame_get_buffer(frame, 32);
        if (ret < 0) {
            std::cout << "Could not allocate the video frame data" << std::endl;
            return nullptr;
        }
        ret = av_frame_make_writable(frame);
        if (ret < 0) {
            std::cout << "Av frame make writable failed." << std::endl;
            return nullptr;
        }
        // 转换颜色空间为 YUV420
        cv::cvtColor(inMat, inMat, cv::COLOR_BGR2YUV_I420);
        // 按 YUV420 格式,设置数据地址
        int frame_size = width * height;
        unsigned char *data = inMat.data;
        memcpy(frame->data[0], data, frame_size);
        memcpy(frame->data[1], data + frame_size, frame_size/4);
        memcpy(frame->data[2], data + frame_size * 5/4, frame_size/4);
        return frame;
    }
    

    2.2 工作流程

    RTSP 输入流 ↓ av_read_frame() ← 拉流 ↓ avcodec_send_packet() avcodec_receive_frame() ← GPU 硬解 (h264_cuvid) 得到 GPU 帧(NV12 on CUDA)// 需要支持软解 ↓ av_hwframe_transfer_data() ← 下载到 CPU 内存 (NV12) ↓ sws_scale() → BGR ← 转 OpenCV 的 BGR 格式 ↓ cv::Mat → 自定义处理(AI 视频识别等) ↓ sws_scale() → YUV420P 或 NV12 ← 转编码器所需格式(CPU) ↓ avcodec_send_frame() ← 送入编码器(可选 GPU 编码:h264_nvenc) ↓ avcodec_receive_packet() ↓ av_write_frame() ← 推送到 RTMP / RTSP / 文件
    

    2.3 工程实现

    extern "C" {
    #include <libavcodec/avcodec.h>
    #include <libavformat/avformat.h>
    #include <libswscale/swscale.h>
    #include <libavutil/error.h>
    #include <libavutil/mem.h>
    #include <libavdevice/avdevice.h>
    #include <libavutil/time.h>
    }
    #include <opencv2/imgproc/imgproc_c.h>
    #include <opencv2/core/opengl.hpp>
    #include <opencv2/cudacodec.hpp>
    #include <opencv2/freetype.hpp>
    #include <opencv2/highgui/highgui.hpp>
    #include <opencv2/opencv.hpp>
    #include <iostream>
    #include <queue>
    #include <string>
    #include <vector>
    
    /// Mat 转 AVFrame
    AVFrame *CVMatToAVFrame(cv::Mat &inMat) {
        // 得到 Mat 信息
        AVPixelFormat dstFormat = AV_PIX_FMT_YUV420P;
        int width = inMat.cols;
        int height = inMat.rows;
        // 创建 AVFrame 填充参数 注:调用者释放该 frame
        AVFrame *frame = av_frame_alloc();
        frame->width = width;
        frame->height = height;
        frame->format = dstFormat;
        // 初始化 AVFrame 内部空间
        int ret = av_frame_get_buffer(frame, 64);
        if (ret < 0) {
            return nullptr;
        }
        ret = av_frame_make_writable(frame);
        if (ret < 0) {
            return nullptr;
        }
        // 转换颜色空间为 YUV420
        cv::cvtColor(inMat, inMat, cv::COLOR_BGR2YUV_I420);
        // 按 YUV420 格式,设置数据地址
        int frame_size = width * height;
        unsigned char *data = inMat.data;
        memcpy(frame->data[0], data, frame_size);
        memcpy(frame->data[1], data + frame_size, frame_size/4);
        memcpy(frame->data[2], data + frame_size * 5/4, frame_size/4);
        return frame;
    }
    
    void rtmpPush2(std::string inputUrl, std::string outputUrl){
        // 视频流下标
        int videoindex = -1;
        // 注册相关服务,可以不用 av_register_all();
        avformat_network_init();
        const char *inUrl = inputUrl.c_str();
        const char *outUrl = outputUrl.c_str();
        // 使用 opencv 解码 VideoCapture capture; cv::Mat frame;
        // 像素转换上下文
        SwsContext *vsc = NULL;
        // 输出的数据结构
        AVFrame *yuv = NULL;
        // 输出编码器上下文
        AVCodecContext *outputVc = NULL;
        // rtmp flv 封装器
        AVFormatContext *output = NULL;
        // 定义描述输入、输出媒体流的构成和基本信息
        AVFormatContext *input_ctx = NULL;
        AVFormatContext * output_ctx = NULL;
    
        /// 1、加载视频流
        // 打开一个输入流并读取报头
        int ret = avformat_open_input(&input_ctx, inUrl, 0, NULL);
        if (ret < 0) {
            std::cout << "avformat_open_input failed!" << std::endl;
            return;
        }
        std::cout << "avformat_open_input success!" << std::endl;
        // 读取媒体文件的数据包以获取流信息
        ret = avformat_find_stream_info(input_ctx, 0);
        if (ret != 0) {
            return;
        }
        // 终端打印相关信息
        av_dump_format(input_ctx, 0, inUrl, 0);
        // 如果是输入文件 flv 可以不传,可以从文件中判断。如果是流则必须传
        ret = avformat_alloc_output_context2(&output_ctx, NULL, "flv", outUrl);
        if (ret < 0) {
            std::cout << "avformat_alloc_output_context2 failed!" << std::endl;
            return;
        }
        std::cout << "avformat_alloc_output_context2 success!" << std::endl;
        std::cout << "nb_streams: " << input_ctx->nb_streams << std::endl;
        unsigned int i;
        for (i = 0; i < input_ctx->nb_streams; i++) {
            AVStream *in_stream = input_ctx->streams[i];
            // 根据输入流创建输出流
            AVStream *out_stream = avformat_new_stream(output_ctx, in_stream->codec->codec);
            if (!out_stream) {
                std::cout << "Failed to successfully add audio and video stream" << std::endl;
                ret = AVERROR_UNKNOWN;
            }
            // 将输入编解码器上下文信息 copy 给输出编解码器上下文
            ret = avcodec_parameters_copy(out_stream->codecpar, in_stream->codecpar);
            if (ret < 0) {
                printf("copy 编解码器上下文失败\n");
            }
            out_stream->codecpar->codec_tag = 0;
            out_stream->codec->codec_tag = 0;
            if (output_ctx->oformat->flags & AVFMT_GLOBALHEADER) {
                out_stream->codec->flags = out_stream->codec->flags | AV_CODEC_FLAG_GLOBAL_HEADER;
            }
        }
        // 查找到当前输入流中的视频流,并记录视频流的索引
        for (i = 0; i < input_ctx->nb_streams; i++) {
            if (input_ctx->streams[i]->codec->codec_type == AVMEDIA_TYPE_VIDEO) {
                videoindex = i;
                break;
            }
        }
        // 获取视频流中的编解码上下文
        AVCodecContext *pCodecCtx = input_ctx->streams[videoindex]->codec;
        // 4.根据编解码上下文中的编码 id 查找对应的解码
        AVCodec *pCodec = avcodec_find_decoder(pCodecCtx->codec_id);
        // pCodec = avcodec_find_decoder_by_name("flv");
        if (pCodec == NULL) {
            std::cout << "找不到解码器" << std::endl;
            return;
        }
        // 5.打开解码器
        if (avcodec_open2(pCodecCtx, pCodec,NULL)<0) {
            std::cout << "解码器无法打开" << std::endl;
            return;
        }
        // 推流每一帧数据
        AVPacket pkt;
        // 获取当前的时间戳 微妙
        long long start_time = av_gettime();
        long long frame_index = 0;
        int count = 0;
        int indexCount = 1;
        int numberAbs = 1;
        bool flag;
        int VideoCount = 0;
        std::queue<std::string> VideoList;
        /// 定义解码过程中相关参数
        AVFrame *pFrame = av_frame_alloc();
        int got_picture;
        int frame_count = 0;
        AVFrame* pFrameYUV = av_frame_alloc();
        uint8_t *out_buffer;
        out_buffer = new uint8_t[avpicture_get_size(AV_PIX_FMT_BGR24, pCodecCtx->width, pCodecCtx->height)];
        avpicture_fill((AVPicture *)pFrameYUV, out_buffer, AV_PIX_FMT_BGR24, pCodecCtx->width, pCodecCtx->height);
        try{
            /// opencv 解码
            // capture.open(inUrl);
            // while (!capture.isOpened()){
            //     capture.open(inUrl);
            //     std::cout << "直播地址无法打开" << endl;
            // }
            // int inWidth = capture.get(CAP_PROP_FRAME_WIDTH);
            // int inHeight = capture.get(CAP_PROP_FRAME_HEIGHT);
            // int fps = capture.get(CAP_PROP_FPS);
            /// TODO: ffmpeg 解码
            /// 2 初始化格式转换上下文
            vsc = sws_getCachedContext(vsc, pCodecCtx->width, pCodecCtx->height, AV_PIX_FMT_RGB24, // 源宽、高、像素格式
                                       pCodecCtx->width, pCodecCtx->height, AV_PIX_FMT_YUV420P, // 目标宽、高、像素格式
                                       SWS_BICUBIC, // 尺寸变化使用算法
                                       0, 0, 0);
            if (!vsc) {
                throw std::logic_error("sws_getCachedContext failed!"); // 转换失败
            }
            /// 3 初始化输出的数据结构
            yuv = av_frame_alloc();
            yuv->format = AV_PIX_FMT_YUV420P;
            yuv->width = pCodecCtx->width;
            yuv->height = pCodecCtx->height;
            yuv->pts = 0;
            // 分配 yuv 空间
            int ret = av_frame_get_buffer(yuv, 32);
            if (ret != 0) {
                char buf[1024] = {0};
                av_strerror(ret, buf, sizeof(buf) - 1);
                throw std::logic_error(buf);
            }
            /// 4 初始化编码上下文
            // a 找到编码器
            AVCodec *codec = avcodec_find_encoder(AV_CODEC_ID_H264);
            if (!codec) {
                throw std::logic_error("Can`t find h264 encoder!"); // 找不到 264 编码器
            }
            // b 创建编码器上下文
            outputVc = avcodec_alloc_context3(codec);
            if (!outputVc) {
                throw std::logic_error("avcodec_alloc_context3 failed!"); // 创建编码器失败
            }
            // c 配置编码器参数
            outputVc->flags |= AV_CODEC_FLAG_GLOBAL_HEADER; // 全局参数
            outputVc->codec_id = codec->id;
            outputVc->thread_count = 8;
            outputVc->bit_rate = 50 * 1024 * 8; // 压缩后每秒视频的 bit 位大小为 50kb
            outputVc->width = pCodecCtx->width;
            outputVc->height = pCodecCtx->height;
            outputVc->time_base = {1, pCodecCtx->time_base.den};
            outputVc->framerate = {pCodecCtx->time_base.den, 1};
            /// TODO 以下参数可以控制直播画质和清晰度
            // 画面组的大小,多少帧一个关键帧
            outputVc->gop_size = 30;
            outputVc->max_b_frames = 1;
            outputVc->qmax = 51;
            outputVc->qmin = 10;
            outputVc->pix_fmt = AV_PIX_FMT_YUV420P;
            // d 打开编码器上下文
            ret = avcodec_open2(outputVc, 0, 0);
            if (ret != 0) {
                char buf[1024] = {0};
                av_strerror(ret, buf, sizeof(buf) - 1);
                throw std::logic_error(buf);
            }
            std::cout << "avcodec_open2 success!" << std::endl;
            /// 5 输出封装器和视频流配置
            // a 创建输出封装器上下文
            ret = avformat_alloc_output_context2(&output, 0, "flv", outUrl);
            if (ret != 0) {
                char buf[1024] = {0};
                av_strerror(ret, buf, sizeof(buf) - 1);
                throw std::logic_error(buf);
            }
            // b 添加视频流
            AVStream *vs = avformat_new_stream(output, codec);
            if (!vs) {
                throw std::logic_error("avformat_new_stream failed");
            }
            vs->codecpar->codec_tag = 0;
            // 从编码器复制参数
            avcodec_parameters_from_context(vs->codecpar, outputVc);
            av_dump_format(output, 0, outUrl, 1);
            // 写入封装头
            ret = avio_open(&output->pb, outUrl, AVIO_FLAG_WRITE);
            ret = avformat_write_header(output, NULL);
            if (ret != 0) {
                std::cout << "ret:" << ret << std::endl;
                char buf[1024] = {0};
                av_strerror(ret, buf, sizeof(buf) - 1);
                throw std::logic_error(buf);
            }
            AVPacket pack;
            memset(&pack, 0, sizeof(pack));
            int vpts = 0;
            bool imgFrameFlag = false;
            int timeCount = 0;
            while (1) {
                /// 读取 rtsp 视频帧(opencv 读取方式,如使用 opencv 方式、则不需要前期的针对输入流的相关处理)
                /* if (!capture.grab()) { continue; }
                /// yuv 转换为 rgb
                if (!capture.retrieve(frame)) { continue; }
                if (frame.empty()){
                    capture.open(inUrl);
                    while (!capture.isOpened()){
                        capture.open(inUrl);
                        std::cout << "直播地址无法打开" << endl;
                    }
                    imgFrameFlag = true;
                }
                if (imgFrameFlag){
                    imgFrameFlag = false;
                    continue;
                } */
                ret = av_read_frame(input_ctx, &pkt);
                if (ret < 0) {
                    continue;
                }
                // 延时
                if (pkt.stream_index == videoindex) {
                    /// TODO: 获取 frame
                    // 7.解码一帧视频压缩数据,得到视频像素数据
                    auto start = std::chrono::system_clock::now();
                    ret = avcodec_decode_video2(pCodecCtx, pFrame, &got_picture, &pkt);
                    auto end = std::chrono::system_clock::now();
                    // cout << "The run time is: "<<(double)(endTime - startTime) / CLOCKS_PER_SEC << "s" << endl;
                    // cout << "Detection time of old version is: "<<std::chrono::duration_cast<std::chrono::milliseconds>(end - start).count() << "ms" << endl;
                    if (ret < 0) {
                        std::cout << ret << std::endl;
                        std::cout << "解码错误" << std::endl;
                        av_frame_free(&yuv);
                        av_free_packet(&pack);
                        continue;
                    }
                    // 为 0 说明解码完成,非 0 正在解码
                    if (got_picture) {
                        SwsContext *img_convert_ctx;
                        img_convert_ctx = sws_getContext(pCodecCtx->width, pCodecCtx->height, pCodecCtx->pix_fmt, pCodecCtx->width, pCodecCtx->height, AV_PIX_FMT_BGR24, SWS_BICUBIC, NULL, NULL, NULL);
                        sws_scale(img_convert_ctx, (const uint8_t *const *) pFrame->data, pFrame->linesize, 0, pCodecCtx->height, pFrameYUV->data, pFrameYUV->linesize);
                        frame_count++;
                        cv::Mat img;
                        img = cv::Mat(pCodecCtx->height, pCodecCtx->width, CV_8UC3);
                        img.data = pFrameYUV->data[0];
                        /// TODO: 图像处理模块
                        /* 此处可根据需要进行相关处理,往视频叠加元素、视频检测 */
                        /// rgb to yuv
                        yuv = CVMatToAVFrame(img);
                        /// h264 编码
                        yuv->pts = vpts;
                        vpts++;
                        ret = avcodec_send_frame(outputVc, yuv);
                        av_frame_free(&yuv);
                        if (ret != 0){
                            av_frame_free(&yuv);
                            av_free_packet(&pack);
                            continue;
                        }
                        ret = avcodec_receive_packet(outputVc, &pack);
                        if (ret != 0 || pack.size > 0) {}
                        else {
                            av_frame_free(&yuv);
                            av_free_packet(&pack);
                            continue;
                        }
                        int firstFrame = 0;
                        if (pack.dts < 0 || pack.pts < 0 || pack.dts > pack.pts || firstFrame) {
                            firstFrame = 0;
                            pack.dts = pack.pts = pack.duration = 0;
                        }
                        // 推流
                        pack.pts = av_rescale_q(pack.pts, outputVc->time_base, vs->time_base);
                        // 显示时间
                        pack.dts = av_rescale_q(pack.dts, outputVc->time_base, vs->time_base);
                        // 解码时间
                        pack.duration = av_rescale_q(pack.duration, outputVc->time_base, vs->time_base);
                        // 数据时长
                        ret = av_interleaved_write_frame(output, &pack);
                        if (ret < 0) {
                            printf("发送数据包出错\n");
                            av_frame_free(&yuv);
                            av_free_packet(&pack);
                            continue;
                        }
                        av_frame_free(&yuv);
                        av_free_packet(&pack);
                    }
                }
            }
        }catch (exception e){
            if (vsc) {
                sws_freeContext(vsc);
                vsc = NULL;
            }
            if (outputVc) {
                avio_closep(&output->pb);
                avcodec_free_context(&outputVc);
            }
            std::cerr << e.what() << std::endl;
        }
    }
    
    int main() {
        av_log_set_level(AV_LOG_TRACE);
        rtmpPush2("rtmp://39.170.104.236:28081/live/456","rtmp://39.170.104.237:28081/live/dj/1ZNBJ7C00C009X");
    }
    

    2.4 常用工具类封装

    ffmpegheader.h

    #ifndef FFMPEGHEADER_H
    #define FFMPEGHEADER_H
    /**
     * 封装常用的 ffmpeg 方法以及类 只需要引入文件即可直接使用 避免重复轮子
     */
    extern "C" {
    #include "./libavcodec/avcodec.h"
    #include "./libavformat/avformat.h"
    #include "./libavformat/avio.h"
    #include "./libavutil/opt.h"
    #include "./libavutil/time.h"
    #include "./libavutil/imgutils.h"
    #include "./libswscale/swscale.h"
    #include "./libswresample/swresample.h"
    #include "./libavutil/avutil.h"
    #include "./libavutil/ffversion.h"
    #include "./libavutil/frame.h"
    #include "./libavutil/pixdesc.h"
    #include "./libavutil/imgutils.h"
    #include "./libavfilter/avfilter.h"
    #include "./libavfilter/buffersink.h"
    #include "./libavfilter/buffersrc.h"
    #include "./libavdevice/avdevice.h"
    }
    #include <iostream>
    
    /************************************* 常用函数封装 ************************************/
    // 获取 ffmpeg 报错信息
    char *getAVError(int errNum);
    // 根据 pts 计算实际时间 us
    int64_t getRealTimeByPTS(int64_t pts, AVRational timebase);
    // pts 转换为 us 差时进行延时
    void calcAndDelay(int64_t startTime, int64_t pts, AVRational timebase);
    // 十六进制字节数组转十进制
    int32_t hexArrayToDec(char *array, int len);
    
    /************************************* 常用类封装 *************************************/
    // 视频图像转换类
    class VideoSwser {
    public:
        VideoSwser();
        ~VideoSwser();
        // 初始化转换器
        bool initSwsCtx(int srcWidth, int srcHeight, AVPixelFormat srcFmt, int dstWidth, int dstHeight, AVPixelFormat dstFmt);
        void release();
        // 返回转换格式后的 AVFrame
        AVFrame *getSwsFrame(AVFrame *srcFrame);
    private:
        bool hasInit;
        bool needSws;
        int dstWidth;
        int dstHeight;
        AVPixelFormat dstFmt;
        // 格式转换
        SwsContext *videoSwsCtx;
    };
    
    // 视频编码器类
    class VideoEncoder {
    public:
        VideoEncoder();
        ~VideoEncoder();
        // 初始化编码器
        bool initEncoder(int width, int height, AVPixelFormat fmt, int fps);
        void release();
        // 返回编码后 AVPacket
        AVPacket *getEncodePacket(AVFrame *srcFrame);
        AVPacket *flushEncoder();
        // 返回编码器上下文
        AVCodecContext *getCodecContent();
    private:
        bool hasInit;
        // 编码器
        AVCodecContext *videoEnCodecCtx;
    };
    
    // 音频重采样类
    class AudioSwrer {
    public:
        AudioSwrer();
        ~AudioSwrer();
        // 初始化转换器
        bool initSwrCtx(int inChannels, int inSampleRate, AVSampleFormat inFmt, int outChannels, int outSampleRate, AVSampleFormat outFmt);
        void release();
        // 返回转换格式后的 AVFrame
        AVFrame *getSwrFrame(AVFrame *srcFrame);
        // 返回转换格式后的 AVFrame,srcData 为一帧源格式的数据
        AVFrame *getSwrFrame(uint8_t *srcData);
    private:
        bool hasInit;
        bool needSwr;
        int outChannels;
        int outSampleRate;
        AVSampleFormat outFmt;
        // 格式转换
        SwrContext *audioSwrCtx;
    };
    
    // 音频编码器类
    class AudioEncoder {
    public:
        AudioEncoder();
        ~AudioEncoder();
        // 初始化编码器
        bool initEncoder(int channels, int sampleRate, AVSampleFormat sampleFmt);
        void release();
        // 返回编码后 AVPacket
        AVPacket *getEncodePacket(AVFrame *srcFrame);
        AVPacket *flushEncoder();
        // 返回编码器上下文
        AVCodecContext *getCodecContent();
    private:
        bool hasInit;
        // 编码器
        AVCodecContext *audioEnCodecCtx;
    };
    
    // 实时采集场景时间戳处理类
    class AVTimeStamp {
    public:
        // 累加帧间隔 优点:时间戳稳定均匀 缺点:实际采集帧率可能不稳定,固定累加或忽略小数会累加误差造成不同步
        // 实时时间戳 优点:时间戳保持实时及正确 缺点:存在帧间隔不均匀,极端情况不能正常播放
        // 累加 + 实时矫正 优点:时间戳实时且较为均匀 缺点:纠正时间戳的某一时刻可能画面或声音卡顿
        enum PTSMode { PTS_RECTIFY = 0, // 默认矫正类型 保持帧间隔尽量均匀
                       PTS_REALTIME // 实时 pts
        };
    public:
        AVTimeStamp();
        ~AVTimeStamp();
        // 初始化时间戳参数
        void initAudioTimeStampParm(int sampleRate, PTSMode mode = PTS_RECTIFY);
        void initVideoTimeStampParm(int fps, PTSMode mode = PTS_RECTIFY);
        // 开始时间戳记录
        void startTimeStamp();
        // 返回 pts
        int64_t getAudioPts();
        int64_t getVideoPts();
    private:
        // 当前模式
        PTSMode aMode;
        PTSMode vMode;
        // 时间戳相关记录 均 us 单位
        int64_t startTime;
        int64_t audioTimeStamp;
        int64_t videoTimeStamp;
        double audioDuration;
        double videoDuration;
    };
    #endif // FFMPEGHEADER_H
    

    ffmpegheader.cpp

    #include "ffmpegheader.h"
    char *getAVError(int errNum) {
        static char msg[32] = {0};
        av_strerror(errNum, msg, 32);
        return msg;
    }
    
    int64_t getRealTimeByPTS(int64_t pts, AVRational timebase) {
        // pts 转换为对应 us 值
        AVRational timebase_q = {1, AV_TIME_BASE};
        int64_t ptsTime = av_rescale_q(pts, timebase, timebase_q);
        return ptsTime;
    }
    
    void calcAndDelay(int64_t startTime, int64_t pts, AVRational timebase) {
        int64_t ptsTime = getRealTimeByPTS(pts, timebase);
        // 计算 startTime 到此刻的时间差值
        int64_t nowTime = av_gettime() - startTime;
        int64_t offset = ptsTime - nowTime;
        // 大于 2 秒一般时间戳存在问题 延时无法挽救
        if(offset > 1000 && offset < 2*1000*1000)
            av_usleep(offset);
    }
    
    int32_t hexArrayToDec(char *array, int len) {
        // 目前限制四字节长度 超过则注意返回类型 防止溢出
        if(array == nullptr || len > 4)
            return -1;
        int32_t result = 0;
        for(int i=0; i<len; i++)
            result = result * 256 + (unsigned char)array[i];
        return result;
    }
    
    VideoSwser::VideoSwser() {
        videoSwsCtx = nullptr;
        hasInit = false;
        needSws = false;
    }
    
    VideoSwser::~VideoSwser() {
        release();
    }
    
    bool VideoSwser::initSwsCtx(int srcWidth, int srcHeight, AVPixelFormat srcFmt, int dstWidth, int dstHeight, AVPixelFormat dstFmt) {
        release();
        if(srcWidth == dstWidth && srcHeight == dstHeight && srcFmt == dstFmt) {
            needSws = false;
        } else {
            // 设置转换上下文 srcFmt 到 dstFmt(一般为 AV_PIX_FMT_YUV420P) 的转换
            videoSwsCtx = sws_getContext(srcWidth, srcHeight, srcFmt, dstWidth, dstHeight, dstFmt, SWS_BILINEAR, NULL, NULL, NULL);
            if (videoSwsCtx == NULL) {
                std::cout << "sws_getContext error" << std::endl;
                return false;
            }
            this->dstFmt = dstFmt;
            this->dstWidth = dstWidth;
            this->dstHeight = dstHeight;
            needSws = true;
        }
        hasInit = true;
        return true;
    }
    
    void VideoSwser::release() {
        if(videoSwsCtx) {
            sws_freeContext(videoSwsCtx);
            videoSwsCtx = nullptr;
        }
        hasInit = false;
        needSws = false;
    }
    
    AVFrame *VideoSwser::getSwsFrame(AVFrame *srcFrame) {
        if(!hasInit) {
            std::cout << "Swser 未初始化" << std::endl;
            return nullptr;
        }
        if(!srcFrame)
            return nullptr;
        if(!needSws)
            return srcFrame;
        AVFrame *frame = av_frame_alloc();
        frame->format = dstFmt;
        frame->width = dstWidth;
        frame->height = dstHeight;
        int ret = av_frame_get_buffer(frame, 0);
        if (ret != 0) {
            std::cout << "av_frame_get_buffer swsFrame error" << std::endl;
            return nullptr;
        }
        ret = av_frame_make_writable(frame);
        if (ret != 0) {
            std::cout << "av_frame_make_writable swsFrame error" << std::endl;
            return nullptr;
        }
        sws_scale(videoSwsCtx, (const uint8_t *const *)srcFrame->data, srcFrame->linesize, 0, dstHeight, frame->data, frame->linesize);
        return frame;
    }
    
    VideoEncoder::VideoEncoder() {
        videoEnCodecCtx = nullptr;
        hasInit = false;
    }
    
    VideoEncoder::~VideoEncoder() {
        release();
    }
    
    bool VideoEncoder::initEncoder(int width, int height, AVPixelFormat fmt, int fps) {
        // 重置编码信息
        release();
        // 设置编码器参数 默认 AV_CODEC_ID_H264
        AVCodec *videoEnCoder = avcodec_find_encoder(AV_CODEC_ID_H264);
        if(!videoEnCoder) {
            std::cout << "avcodec_find_encoder AV_CODEC_ID_H264 error" << std::endl;
            return false;
        }
        videoEnCodecCtx = avcodec_alloc_context3(videoEnCoder);
        if(!videoEnCodecCtx) {
            std::cout << "avcodec_alloc_context3 AV_CODEC_ID_H264 error" << std::endl;
            return false;
        }
        // 重要!编码参数设置 应根据实际场景修改以下参数
        videoEnCodecCtx->bit_rate = 2*1024*1024; // 1080P:4Mbps 720P:2Mbps 480P:1Mbps 默认中等码率可适当增大
        videoEnCodecCtx->width = width;
        videoEnCodecCtx->height = height;
        videoEnCodecCtx->framerate = {fps, 1};
        videoEnCodecCtx->time_base = {1, AV_TIME_BASE};
        videoEnCodecCtx->gop_size = fps;
        videoEnCodecCtx->max_b_frames = 0;
        videoEnCodecCtx->pix_fmt = fmt;
        videoEnCodecCtx->thread_count = 2;
        videoEnCodecCtx->thread_type = FF_THREAD_FRAME;
        // 设置 QP 最大和最小量化系数,取值范围为 0~51 越大编码质量越差
        videoEnCodecCtx->qmin = 10;
        videoEnCodecCtx->qmax = 30;
        // 若设置此项 则 sps、pps 将保存在 extradata;否则放置于每个 I 帧前
        videoEnCodecCtx->flags |= AV_CODEC_FLAG_GLOBAL_HEADER;
        // 预设参数 编码速度与压缩率的平衡 如编码快选择的算法就偏简单 压缩率低
        // 由慢到快 veryslow slower slow medium fast faster veryfast superfast ultrafast 默认 medium
        int ret = av_opt_set(videoEnCodecCtx->priv_data, "preset", "ultrafast", 0);
        if(ret != 0)
            std::cout << "av_opt_set preset error" << std::endl;
        // 偏好设置 进行视觉优化
        // film 电影 animation 动画片 grain 颗粒物 stillimage 静止图片 psnr ssim 图像评价指标 fastdecode 快速解码 zerolatency 零延迟
        ret = av_opt_set(videoEnCodecCtx->priv_data, "tune", "zerolatency", 0);
        if(ret != 0)
            std::cout << "av_opt_set preset error" << std::endl;
        // 画质设置 可能自动改变 如编码很快很难保证高画质会自动降级
        // baseline 实时通信 extended 使用较少 main 流媒体 high 广电、存储
        ret = av_opt_set(videoEnCodecCtx->priv_data, "profile", "main", 0);
        if(ret != 0)
            std::cout << "av_opt_set preset error" << std::endl;
        ret = avcodec_open2(videoEnCodecCtx, videoEnCoder, NULL);
        if(ret != 0) {
            std::cout << "avcodec_open2 video error" << std::endl;
            return false;
        }
        hasInit = true;
        return true;
    }
    
    void VideoEncoder::release() {
        if(videoEnCodecCtx) {
            avcodec_free_context(&videoEnCodecCtx);
            videoEnCodecCtx = nullptr;
        }
        hasInit = false;
    }
    
    AVPacket *VideoEncoder::getEncodePacket(AVFrame *srcFrame) {
        if(!hasInit) {
            std::cout << "VideoEncoder no init" << std::endl;
            return nullptr;
        }
        if(!srcFrame)
            return nullptr;
        if(srcFrame->width != videoEnCodecCtx->width || srcFrame->height != videoEnCodecCtx->height || srcFrame->format != videoEnCodecCtx->pix_fmt) {
            std::cout << "srcFrame 不符合视频编码器设置格式" << std::endl;
            return nullptr;
        }
        // 应保证 srcFrame pts 为 us 单位
        srcFrame->pts = av_rescale_q(srcFrame->pts, AVRational{1, AV_TIME_BASE}, videoEnCodecCtx->time_base);
        int ret = avcodec_send_frame(videoEnCodecCtx, srcFrame);
        if (ret != 0)
            return nullptr;
        // 接收者负责释放 packet
        AVPacket *packet = av_packet_alloc();
        ret = avcodec_receive_packet(videoEnCodecCtx, packet);
        if (ret != 0) {
            av_packet_free(&packet);
            return nullptr;
        }
        return packet;
    }
    
    AVPacket *VideoEncoder::flushEncoder() {
        if(!hasInit) {
            std::cout << "VideoEncoder no init" << std::endl;
            return nullptr;
        }
        int ret = avcodec_send_frame(videoEnCodecCtx, NULL);
        if (ret != 0)
            return nullptr;
        // 接收者负责释放 packet
        AVPacket *packet = av_packet_alloc();
        ret = avcodec_receive_packet(videoEnCodecCtx, packet);
        if (ret != 0) {
            av_packet_free(&packet);
            return nullptr;
        }
        return packet;
    }
    
    AVCodecContext *VideoEncoder::getCodecContent() {
        return videoEnCodecCtx;
    }
    
    AudioSwrer::AudioSwrer() {
        audioSwrCtx = nullptr;
        hasInit = false;
        needSwr = false;
    }
    
    AudioSwrer::~AudioSwrer() {
        release();
    }
    
    bool AudioSwrer::initSwrCtx(int inChannels, int inSampleRate, AVSampleFormat inFmt, int outChannels, int outSampleRate, AVSampleFormat outFmt) {
        release();
        if(inChannels == outChannels && inSampleRate == outSampleRate && inFmt == outFmt) {
            needSwr = false;
        } else {
            audioSwrCtx = swr_alloc_set_opts(NULL, av_get_default_channel_layout(outChannels), outFmt, outSampleRate, av_get_default_channel_layout(inChannels), inFmt, inSampleRate, 0, NULL);
            if (!audioSwrCtx) {
                std::cout << "swr_alloc_set_opts failed!" << std::endl;
                return false;
            }
            int ret = swr_init(audioSwrCtx);
            if (ret != 0) {
                std::cout << "swr_init error" << std::endl;
                swr_free(&audioSwrCtx);
                return false;
            }
            this->outFmt = outFmt;
            this->outChannels = outChannels;
            this->outSampleRate = outSampleRate;
            needSwr = true;
        }
        hasInit = true;
        return true;
    }
    
    void AudioSwrer::release() {
        if(audioSwrCtx) {
            swr_free(&audioSwrCtx);
            audioSwrCtx = nullptr;
        }
        hasInit = false;
        needSwr = false;
    }
    
    AVFrame *AudioSwrer::getSwrFrame(AVFrame *srcFrame) {
        if(!hasInit) {
            std::cout << "Swrer 未初始化" << std::endl;
            return nullptr;
        }
        if(!srcFrame)
            return nullptr;
        if(!needSwr)
            return srcFrame;
        AVFrame *frame = av_frame_alloc();
        frame->format = outFmt;
        frame->channels = outChannels;
        frame->channel_layout = av_get_default_channel_layout(outChannels);
        frame->nb_samples = 1024; // 默认 aac
        int ret = av_frame_get_buffer(frame, 0);
        if (ret != 0) {
            std::cout << "av_frame_get_buffer audio error" << std::endl;
            return nullptr;
        }
        ret = av_frame_make_writable(frame);
        if (ret != 0) {
            std::cout << "av_frame_make_writable swrFrame error" << std::endl;
            return nullptr;
        }
        const uint8_t **inData = (const uint8_t **)srcFrame->data;
        swr_convert(audioSwrCtx, frame->data, frame->nb_samples, inData, frame->nb_samples);
        return frame;
    }
    
    AVFrame *AudioSwrer::getSwrFrame(uint8_t *srcData) {
        if(!hasInit) {
            std::cout << "Swrer 未初始化" << std::endl;
            return nullptr;
        }
        if(!srcData)
            return nullptr;
        if(!needSwr)
            return nullptr;
        AVFrame *frame = av_frame_alloc();
        frame->format = outFmt;
        frame->channels = outChannels;
        frame->sample_rate = outSampleRate;
        frame->channel_layout = av_get_default_channel_layout(outChannels);
        frame->nb_samples = 1024; // 默认 aac
        int ret = av_frame_get_buffer(frame, 0);
        if (ret != 0) {
            std::cout << "av_frame_get_buffer audio error" << std::endl;
            return nullptr;
        }
        ret = av_frame_make_writable(frame);
        if (ret != 0) {
            std::cout << "av_frame_make_writable swrFrame error" << std::endl;
            return nullptr;
        }
        const uint8_t *indata[AV_NUM_DATA_POINTERS] = {0};
        indata[0] = srcData;
        swr_convert(audioSwrCtx, frame->data, frame->nb_samples, indata, frame->nb_samples);
        return frame;
    }
    
    AudioEncoder::AudioEncoder() {
        audioEnCodecCtx = nullptr;
        hasInit = false;
    }
    
    AudioEncoder::~AudioEncoder() {
        release();
    }
    
    bool AudioEncoder::initEncoder(int channels, int sampleRate, AVSampleFormat sampleFmt) {
        release();
        // 初始化音频编码器相关 默认 AAC
        AVCodec *audioEnCoder = avcodec_find_encoder(AV_CODEC_ID_AAC);
        if (!audioEnCoder) {
            std::cout << "avcodec_find_encoder AV_CODEC_ID_AAC failed!" << std::endl;
            return false;
        }
        audioEnCodecCtx = avcodec_alloc_context3(audioEnCoder);
        if (!audioEnCodecCtx) {
            std::cout << "avcodec_alloc_context3 AV_CODEC_ID_AAC failed!" << std::endl;
            return false;
        }
        // ffmpeg -h encoder=aac 自带编码器仅支持 AV_SAMPLE_FMT_FLTP 大多数 AAC 编码器都采用平面布局格式 提高数据访问效率和缓存命中率 加快编码效率
        // 音频数据量偏小 设置较为简单
        audioEnCodecCtx->bit_rate = 64*1024;
        audioEnCodecCtx->time_base = AVRational{1, sampleRate};
        audioEnCodecCtx->sample_rate = sampleRate;
        audioEnCodecCtx->sample_fmt = sampleFmt;
        audioEnCodecCtx->channels = channels;
        audioEnCodecCtx->channel_layout = av_get_default_channel_layout(channels);
        audioEnCodecCtx->frame_size = 1024;
        // 打开音频编码器
        int ret = avcodec_open2(audioEnCodecCtx, audioEnCoder, NULL);
        if (ret != 0) {
            std::cout << "avcodec_open2 audio error" << getAVError(ret) << std::endl;
            return false;
        }
        hasInit = true;
        return true;
    }
    
    void AudioEncoder::release() {
        if(audioEnCodecCtx) {
            avcodec_free_context(&audioEnCodecCtx);
            audioEnCodecCtx = nullptr;
        }
        hasInit = false;
    }
    
    AVPacket *AudioEncoder::getEncodePacket(AVFrame *srcFrame) {
        if(!hasInit) {
            std::cout << "AudioEncoder no init" << std::endl;
            return nullptr;
        }
        if(!srcFrame)
            return nullptr;
        if(srcFrame->channels != audioEnCodecCtx->channels || srcFrame->sample_rate != audioEnCodecCtx->sample_rate || srcFrame->format != audioEnCodecCtx->sample_fmt) {
            std::cout << "srcFrame 不符合音频编码器设置格式" << std::endl;
            return nullptr;
        }
        // 应保证 srcFrame pts 为 us 单位
        srcFrame->pts = av_rescale_q(srcFrame->pts, AVRational{1, AV_TIME_BASE}, audioEnCodecCtx->time_base);
        // 进行音频编码得到编码数据 AVPacket
        int ret = avcodec_send_frame(audioEnCodecCtx, srcFrame);
        if (ret != 0)
            return nullptr;
        // 接收者负责释放 packet
        AVPacket *packet = av_packet_alloc();
        ret = avcodec_receive_packet(audioEnCodecCtx, packet);
        if (ret != 0) {
            av_packet_free(&packet);
            return nullptr;
        }
        return packet;
    }
    
    AVPacket *AudioEncoder::flushEncoder() {
        if(!hasInit) {
            std::cout << "AudioEncoder no init" << std::endl;
            return nullptr;
        }
        int ret = avcodec_send_frame(audioEnCodecCtx, NULL);
        if (ret != 0)
            return nullptr;
        // 接收者负责释放 packet
        AVPacket *packet = av_packet_alloc();
        ret = avcodec_receive_packet(audioEnCodecCtx, packet);
        if (ret != 0) {
            av_packet_free(&packet);
            return nullptr;
        }
        return packet;
    }
    
    AVCodecContext *AudioEncoder::getCodecContent() {
        return audioEnCodecCtx;
    }
    
    AVTimeStamp::AVTimeStamp() {
        aMode = PTS_RECTIFY;
        vMode = PTS_RECTIFY;
        startTime = 0;
        audioTimeStamp = 0;
        videoTimeStamp = 0;
        // 默认视频 264 编码 25 帧
        videoDuration = 1000000 / 25;
        // 默认音频 aac 编码 44100 采样率
        audioDuration = 1000000 / (44100 / 1024);
    }
    
    AVTimeStamp::~AVTimeStamp() {
    }
    
    void AVTimeStamp::initAudioTimeStampParm(int sampleRate, AVTimeStamp::PTSMode mode) {
        aMode = mode;
        audioDuration = 1000000 / (sampleRate / 1024);
    }
    
    void AVTimeStamp::initVideoTimeStampParm(int fps, AVTimeStamp::PTSMode mode) {
        vMode = mode;
        videoDuration = 1000000 / fps;
    }
    
    void AVTimeStamp::startTimeStamp() {
        audioTimeStamp = 0;
        videoTimeStamp = 0;
        startTime = av_gettime();
    }
    
    int64_t AVTimeStamp::getAudioPts() {
        if(aMode == PTS_RECTIFY) {
            int64_t elapsed = av_gettime() - startTime;
            uint32_t offset = qAbs(elapsed - (audioTimeStamp + audioDuration));
            if(offset < (audioDuration * 0.5))
                audioTimeStamp += audioDuration;
            else
                audioTimeStamp = elapsed;
        } else {
            audioTimeStamp = av_gettime() - startTime;
        }
        return audioTimeStamp;
    }
    
    int64_t AVTimeStamp::getVideoPts() {
        if(vMode == PTS_RECTIFY) {
            int64_t elapsed = av_gettime() - startTime;
            uint32_t offset = qAbs(elapsed - (videoTimeStamp + videoDuration));
            if(offset < (videoDuration * 0.5))
                videoTimeStamp += videoDuration;
            else
                videoTimeStamp = elapsed;
        } else {
            videoTimeStamp = av_gettime() - startTime;
        }
        return videoTimeStamp;
    }
    

    目录

    1. 基础知识
    2. 封装和解封装
    3. 编码与解码
    4. 硬件加速编解码
    5. 工具介绍
    6. 工程项目实践
    7. 2.1 格式转换
    8. 2.1.1 AVFrame 转 Mat
    9. 2.1.2 Mat 转 AVFrame
    10. 2.2 工作流程
    11. 2.3 工程实现
    12. 2.4 常用工具类封装
    • 💰 8折买阿里云服务器限时8折了解详情
    • Magick API 一键接入全球大模型注册送1000万token查看
    • 🤖 一键搭建Deepseek满血版了解详情
    • 一键打造专属AI 智能体了解详情
    极客日志微信公众号二维码

    微信扫一扫,关注极客日志

    微信公众号「极客日志V2」,在微信中扫描左侧二维码关注。展示文案:极客日志V2 zeeklog

    更多推荐文章

    查看全部
    • OpenClaw 飞书机器人配置:群消息免@自动回复
    • Cursor 编辑器安装与 Unity-MCP 插件集成指南
    • AI Agent 生产级框架设计与实战落地
    • Pico 4XVR 1.10.13 安装包下载与安装教程
    • 2024-2025 年人工智能领域重要综述论文汇总
    • QClaw 本地化 AI 个人助手平台使用指南
    • Neo4j 5.26 版本下载安装配置步骤
    • Python 推导式底层实现:从语法糖到 CPython 字节码分析
    • Git Config 核心配置详解:层级、命令与避坑
    • 基于 LangChain 构建具备记忆功能的聊天机器人
    • 基于 AI 的智能算力分配与云原生基础设施实践
    • 面试题 17.19 消失的两个数字:位运算实战解析
    • AI 智能体研发之路 - 工程篇(二):Dify 智能体开发平台一键部署
    • OpenClaw 多平台卸载指南:Windows、macOS、Linux 及包管理器
    • 在 WSL2 Ubuntu 上部署 llama.cpp
    • 2026 年 3 月科技圈大事:AI 智能体元年与硬件平权
    • 昇腾 A2 部署 Pi0 机器人大模型:CANN 环境实测
    • 行星减速器:结构原理、计算与 C++ 实现
    • HTML5 结合 AI 实现智能场景渲染技术指南
    • 如何免费使用 AI 绘画模型 Nano Banana Pro

    相关免费在线工具

    • 加密/解密文本

      使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online

    • RSA密钥对生成器

      生成新的随机RSA私钥和公钥pem证书。 在线工具,RSA密钥对生成器在线工具,online

    • Mermaid 预览与可视化编辑

      基于 Mermaid.js 实时预览流程图、时序图等图表,支持源码编辑与即时渲染。 在线工具,Mermaid 预览与可视化编辑在线工具,online

    • 随机西班牙地址生成器

      随机生成西班牙地址(支持马德里、加泰罗尼亚、安达卢西亚、瓦伦西亚筛选),支持数量快捷选择、显示全部与下载。 在线工具,随机西班牙地址生成器在线工具,online

    • Gemini 图片去水印

      基于开源反向 Alpha 混合算法去除 Gemini/Nano Banana 图片水印,支持批量处理与下载。 在线工具,Gemini 图片去水印在线工具,online

    • Base64 字符串编码/解码

      将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online