Windonws 视频存储,10s/不限时

使用的FFMpeng库,库连接:https://download.csdn.net/download/yuchunhai321/92903428

使用的QT工程,编译器MSVC2015,编译成功,运行时需要将库里bin文件下的文件拷到可执行的文件目录下

工程源码:

pro文件中增加

复制代码
INCLUDEPATH += $$PWD/FFMpegH264/lib/ffmpeg4.0.1/include

LIBS += -L$$PWD/FFMpegH264/lib/ffmpeg4.0.1/lib/win32/ -lavcodec -lavdevice -lavfilter -lavformat -lavutil -lpostproc -lswresample -lswscale

videorecorder.h
#ifndef VIDEO_RECORDER_H
#define VIDEO_RECORDER_H

#include <QObject>
#include <QImage>
#include <QThread>
#include <QMutex>
#include <QQueue>
#include <QWaitCondition>

extern "C" {
#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include <libswscale/swscale.h>
#include <libavutil/avutil.h>
#include <libavutil/imgutils.h>
#include <libavutil/time.h>
}

class VideoRecorder : public QObject
{
    Q_OBJECT
public:
    explicit VideoRecorder(QObject *parent = nullptr);
    ~VideoRecorder();

    // 设置编码参数(可选,有默认值)
    void setVideoParameters(int width, int height, int fps = 25, int bitrate = 2000000);

    // 开始录制:传入输出文件名
    bool startRecording(const QString& filename);

    // 停止录制
    void stopRecording();

    // 添加一帧图片数据
    void addFrame(const QImage& image);
    void addFrame(const QByteArray& imageData);

signals:
    // 录制状态信号
    void recordingStarted();
    void recordingStopped(bool success);
    void errorOccurred(const QString& error);

    // 进度信号(可选)
    void frameEncoded(int frameCount);

private:
    // FFmpeg 相关成员
    AVFormatContext* m_formatCtx = nullptr;
    AVCodecContext* m_codecCtx = nullptr;
    AVStream* m_stream = nullptr;
    SwsContext* m_swsCtx = nullptr;
    AVFrame* m_frame = nullptr;

    // 视频参数
    int m_width = 1920;
    int m_height = 1080;
    int m_fps = 25;
    int m_bitrate = 2000000;  // 2 Mbps
    AVPixelFormat m_pixelFormat = AV_PIX_FMT_YUV420P;

    // 编码状态
    bool m_isRecording = false;
    int64_t m_ptsCounter = 0;
    int m_videoStreamIndex = -1;

    // 线程安全的数据队列
    QQueue<QImage> m_frameQueue;
    QMutex m_mutex;
    QWaitCondition m_condition;
    bool m_stopRequested = false;

    // 工作线程
    QThread* m_workerThread = nullptr;

    // 内部函数
    bool initializeFFmpeg(const QString& filename);
    void cleanupFFmpeg();
    bool encodeFrame(const QImage& image);
    void flushEncoder();
    bool convertImageToFrame(const QImage& image, AVFrame* frame);

private slots:
    void processFrames();  // 工作线程的主循环
};

#endif // VIDEO_RECORDER_H

videorecorder.cpp
#include "videorecorder.h"
#include <QDebug>
#include <QDir>

VideoRecorder::VideoRecorder(QObject *parent) : QObject(parent)
{
    // 初始化 FFmpeg 库
    avformat_network_init();

    // 重要:先移除父对象,然后才能移动线程
    this->setParent(nullptr);

    // 创建工作线程
    m_workerThread = new QThread(this);  // 这个可以保留父对象
    this->moveToThread(m_workerThread);

    // 连接信号和槽
    connect(m_workerThread, &QThread::started, this, &VideoRecorder::processFrames);
    connect(m_workerThread, &QThread::finished, m_workerThread, &QThread::deleteLater);

    m_workerThread->start();
}

VideoRecorder::~VideoRecorder()
{
    stopRecording();

    if (m_workerThread) {
        m_workerThread->quit();
        // 注意:这里不能直接调用 wait(),因为当前对象可能在主线程
        // 而 m_workerThread 是子对象,Qt 会自动处理
        // 但为了安全,可以等待一段时间
        if (!m_workerThread->wait(3000)) {
            qDebug() << "Thread didn't stop gracefully, terminating...";
            m_workerThread->terminate();
            m_workerThread->wait();
        }
    }

    cleanupFFmpeg();
    avformat_network_deinit();
}

void VideoRecorder::setVideoParameters(int width, int height, int fps, int bitrate)
{
    m_width = width;
    m_height = height;
    m_fps = fps;
    m_bitrate = bitrate;
}

bool VideoRecorder::startRecording(const QString& filename)
{
    QMutexLocker locker(&m_mutex);

    if (m_isRecording) {
        emit errorOccurred("Already recording");
        return false;
    }

    if (!initializeFFmpeg(filename)) {
        emit errorOccurred("Failed to initialize FFmpeg");
        return false;
    }

    m_isRecording = true;
    m_stopRequested = false;
    m_ptsCounter = 0;

    emit recordingStarted();
    qDebug() << "Recording started:" << filename;

    return true;
}

void VideoRecorder::stopRecording()
{
    if (!m_isRecording) {
        return;
    }

    // 标记停止请求
    {
        QMutexLocker locker(&m_mutex);
        m_stopRequested = true;
        m_condition.wakeAll();
    }
}

void VideoRecorder::addFrame(const QImage& image)
{
    if (!m_isRecording) {
        return;
    }

    // 检查队列大小,避免内存爆炸
    {
        QMutexLocker locker(&m_mutex);
        if (m_frameQueue.size() > 300) {  // 最多缓存300帧
            return;
        }
    }

    // 将图片转换为标准格式
    QImage standardized = image;
    if (standardized.format() != QImage::Format_RGB32 &&
        standardized.format() != QImage::Format_ARGB32) {
        standardized = standardized.convertToFormat(QImage::Format_RGB32);
    }

    // 缩放图片到目标尺寸
    if (standardized.width() != m_width || standardized.height() != m_height) {
        standardized = standardized.scaled(m_width, m_height,
                                          Qt::IgnoreAspectRatio,
                                          Qt::SmoothTransformation);
    }

    QMutexLocker locker(&m_mutex);
    m_frameQueue.enqueue(standardized);
    m_condition.wakeOne();  // 唤醒工作线程
}

void VideoRecorder::addFrame(const QByteArray &imageData)
{
    // 将 QByteArray 转换为 QImage
    QImage image;
    if (!image.loadFromData(imageData)) {
        qDebug() << "Error: Failed to load image from QByteArray";
        return ;
    }

    // 调用原有的 addFrame 函数
    return addFrame(image);
}

bool VideoRecorder::initializeFFmpeg(const QString& filename)
{
    int ret = 0;

    // 确保先清理之前的资源
    cleanupFFmpeg();

    // 1. 创建输出上下文
    ret = avformat_alloc_output_context2(&m_formatCtx, nullptr, "mp4",
                                          filename.toStdString().c_str());
    if (ret < 0 || !m_formatCtx) {
        char errbuf[256];
        av_strerror(ret, errbuf, sizeof(errbuf));
        qDebug() << "Failed to create output context:" << errbuf;
        return false;
    }

    // 2. 查找编码器 (H.264)
    const AVCodec* codec = avcodec_find_encoder(AV_CODEC_ID_H264);
    if (!codec) {
        qDebug() << "H.264 encoder not found, trying MPEG-4...";
        codec = avcodec_find_encoder(AV_CODEC_ID_MPEG4);
        if (!codec) {
            qDebug() << "No suitable encoder found";
            return false;
        }
    }

    // 3. 创建视频流
    m_stream = avformat_new_stream(m_formatCtx, codec);
    if (!m_stream) {
        qDebug() << "Failed to create video stream";
        return false;
    }
    m_videoStreamIndex = m_stream->index;

    // 4. 创建编码器上下文
    m_codecCtx = avcodec_alloc_context3(codec);
    if (!m_codecCtx) {
        qDebug() << "Failed to allocate codec context";
        return false;
    }

    // 5. 设置编码参数
    m_codecCtx->codec_id = codec->id;
    m_codecCtx->codec_type = AVMEDIA_TYPE_VIDEO;
    m_codecCtx->width = m_width;
    m_codecCtx->height = m_height;
    m_codecCtx->time_base = AVRational{1, m_fps};
    m_codecCtx->framerate = AVRational{m_fps, 1};
    m_codecCtx->pix_fmt = m_pixelFormat;
    m_codecCtx->bit_rate = m_bitrate;
    m_codecCtx->gop_size = m_fps * 2;

    // 设置编码质量(对 MPEG-4 有效)
    if (codec->id == AV_CODEC_ID_MPEG4) {
        m_codecCtx->qmin = 10;
        m_codecCtx->qmax = 31;
    }

    // 6. 设置全局头(MP4 需要)
    if (m_formatCtx->oformat->flags & AVFMT_GLOBALHEADER) {
        m_codecCtx->flags |= AV_CODEC_FLAG_GLOBAL_HEADER;
    }

    // 7. 打开编码器
    // 添加编码器选项,减少内部缓冲
    AVDictionary* opts = nullptr;
    av_dict_set(&opts, "tune", "zerolatency", 0);  // 零延迟模式
    av_dict_set(&opts, "preset", "ultrafast", 0);  // 最快编码
    av_dict_set(&opts, "rc_lookahead", "0", 0);    // 无前瞻缓冲

    ret = avcodec_open2(m_codecCtx, codec, &opts);
    if (ret < 0) {
        char errbuf[256];
        av_strerror(ret, errbuf, sizeof(errbuf));
        qDebug() << "Failed to open codec:" << errbuf;
        return false;
    }

    // 8. 将编码器参数复制到流
    ret = avcodec_parameters_from_context(m_stream->codecpar, m_codecCtx);
    if (ret < 0) {
        qDebug() << "Failed to copy codec parameters";
        return false;
    }

    // 9. 创建 AVFrame
    m_frame = av_frame_alloc();
    if (!m_frame) {
        qDebug() << "Failed to allocate frame";
        return false;
    }
    m_frame->format = m_codecCtx->pix_fmt;
    m_frame->width = m_width;
    m_frame->height = m_height;
    ret = av_frame_get_buffer(m_frame, 32);
    if (ret < 0) {
        qDebug() << "Failed to allocate frame buffer";
        return false;
    }

    // 10. 创建图像转换上下文 (RGB -> YUV)
    m_swsCtx = sws_getContext(m_width, m_height, AV_PIX_FMT_RGB32,
                              m_width, m_height, m_pixelFormat,
                              SWS_BILINEAR, nullptr, nullptr, nullptr);
    if (!m_swsCtx) {
        qDebug() << "Failed to create sws context";
        return false;
    }

    // 11. 打开输出文件
    ret = avio_open(&m_formatCtx->pb, filename.toStdString().c_str(), AVIO_FLAG_WRITE);
    if (ret < 0) {
        char errbuf[256];
        av_strerror(ret, errbuf, sizeof(errbuf));
        qDebug() << "Failed to open output file:" << errbuf;
        return false;
    }

    // 12. 写入文件头
    ret = avformat_write_header(m_formatCtx, nullptr);
    if (ret < 0) {
        char errbuf[256];
        av_strerror(ret, errbuf, sizeof(errbuf));
        qDebug() << "Failed to write header:" << errbuf;
        return false;
    }

    return true;
}

void VideoRecorder::cleanupFFmpeg()
{
    if (m_swsCtx) {
        sws_freeContext(m_swsCtx);
        m_swsCtx = nullptr;
    }

    if (m_frame) {
        av_frame_free(&m_frame);
        m_frame = nullptr;
    }

    if (m_codecCtx) {
        avcodec_free_context(&m_codecCtx);
        m_codecCtx = nullptr;
    }

    if (m_formatCtx) {
        if (m_formatCtx->pb) {
            avio_closep(&m_formatCtx->pb);
        }
        avformat_free_context(m_formatCtx);
        m_formatCtx = nullptr;
    }
}

bool VideoRecorder::encodeFrame(const QImage& image)
{
    if (!m_isRecording || !m_codecCtx) {
        return false;
    }

    // 1. 转换图片数据到 AVFrame
    if (!convertImageToFrame(image, m_frame)) {
        return false;
    }

    // 2. 设置 PTS
    m_frame->pts = m_ptsCounter++;

    // 3. 发送帧到编码器
    int ret = avcodec_send_frame(m_codecCtx, m_frame);
    if (ret < 0) {
        char errbuf[256];
        av_strerror(ret, errbuf, sizeof(errbuf));
        qDebug() << "Error sending frame to encoder:" << errbuf;
        return false;
    }

    // 4. 接收编码后的数据包
    AVPacket* pkt = av_packet_alloc();
    bool success = true;

    while (true) {
        ret = avcodec_receive_packet(m_codecCtx, pkt);
        if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {
            break;
        } else if (ret < 0) {
            char errbuf[256];
            av_strerror(ret, errbuf, sizeof(errbuf));
            qDebug() << "Error receiving packet:" << errbuf;
            success = false;
            break;
        }

        // 5. 转换时间戳
        pkt->stream_index = m_videoStreamIndex;
        av_packet_rescale_ts(pkt, m_codecCtx->time_base,
                            m_stream->time_base);

        // 6. 写入文件
        ret = av_interleaved_write_frame(m_formatCtx, pkt);
        if (ret < 0) {
            char errbuf[256];
            av_strerror(ret, errbuf, sizeof(errbuf));
            qDebug() << "Error writing packet:" << errbuf;
            success = false;
        }

        av_packet_unref(pkt);
    }
    av_packet_free(&pkt);

    if (success) {
        emit frameEncoded(m_ptsCounter);
    }

    return success;
}

bool VideoRecorder::convertImageToFrame(const QImage& image, AVFrame* frame)
{
    // 确保 frame 可写
    if (av_frame_make_writable(frame) < 0) {
        return false;
    }

    // 准备源数据
    uint8_t* srcData[1] = { const_cast<uint8_t*>(image.bits()) };
    int srcLinesize[1] = { static_cast<int>(image.bytesPerLine()) };

    // 执行转换
    sws_scale(m_swsCtx, srcData, srcLinesize, 0, m_height,
              frame->data, frame->linesize);

    return true;
}

void VideoRecorder::flushEncoder()
{
    if (!m_codecCtx) {
        return;
    }

    // 发送 NULL 帧刷新编码器
    int ret = avcodec_send_frame(m_codecCtx, nullptr);
    if (ret < 0) {
        return;
    }

    // 接收所有剩余的编码数据包
    AVPacket* pkt = av_packet_alloc();

    while (true) {
        ret = avcodec_receive_packet(m_codecCtx, pkt);
        if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {
            break;
        } else if (ret < 0) {
            break;
        }

        pkt->stream_index = m_videoStreamIndex;
        av_packet_rescale_ts(pkt, m_codecCtx->time_base,
                            m_stream->time_base);
        av_interleaved_write_frame(m_formatCtx, pkt);
        av_packet_unref(pkt);
    }
    av_packet_free(&pkt);

    // 写入文件尾部
    if (m_formatCtx) {
        av_write_trailer(m_formatCtx);
    }
}

void VideoRecorder::processFrames()
{
    while (true) {
        QImage frame;

        {
            QMutexLocker locker(&m_mutex);

            // 等待条件:有帧要处理,或者被要求停止
            while (m_frameQueue.isEmpty() && !m_stopRequested) {
                m_condition.wait(&m_mutex);
            }

            // 检查退出条件
            if (m_stopRequested && m_frameQueue.isEmpty()) {
                break;
            }

            // 取出一帧
            if (!m_frameQueue.isEmpty()) {
                frame = m_frameQueue.dequeue();
            }
        }

        // 编码帧
        if (m_isRecording && !frame.isNull()) {
            if (!encodeFrame(frame)) {
                qDebug() << "Failed to encode frame";
            }
        }
    }

    // 清理:刷新编码器并关闭文件
    if (m_isRecording) {
        qDebug() << "Flushing encoder...";
        flushEncoder();
        cleanupFFmpeg();
        m_isRecording = false;
        emit recordingStopped(true);
        qDebug() << "Recording finished, total frames:" << m_ptsCounter;
    }
}

widget.h
#ifndef WIDGET_H
#define WIDGET_H

#include <QWidget>
#include <QTimer>

#include "videorecorder.h"

namespace Ui {
class Widget;
}

class Widget : public QWidget
{
    Q_OBJECT

public:
    explicit Widget(QWidget *parent = nullptr);
    ~Widget();

private slots:
    void on_pushButton_clicked();
    void onTimeout_10s(); //只录制10s
    void onTimeout_noRestrictions(); //不限制时间
    void onRecordingStopped(bool success);

    void on_pushButton_2_clicked();

private:
    void videoRecorder();

    Ui::Widget *ui;

    VideoRecorder* m_recorder = nullptr;
    QTimer* m_timer = nullptr;
    int m_frameCount = 0;
};

#endif // WIDGET_H

widget.cpp
#include "widget.h"
#include "ui_widget.h"

#include <QDebug>
#include <QTime>
#include <QRandomGenerator>

extern "C"
{
#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include <libavutil/avutil.h>
}

Widget::Widget(QWidget *parent) :
    QWidget(parent),
    ui(new Ui::Widget)
{
    ui->setupUi(this);

    qDebug() << "FFmpeg configuration:" << avcodec_configuration();
    qDebug() << "FFmpeg version:" << avcodec_version();

    // 初始化随机数生成器
    QRandomGenerator::global()->generate();

    // 创建录制器
    m_timer = new QTimer(this);
}

Widget::~Widget()
{
    delete ui;

    if (m_recorder) {
        m_recorder->stopRecording();
        m_recorder->deleteLater();
    }

    if (m_timer) {
        m_timer->stop();
        delete m_timer;
    }
}

void Widget::on_pushButton_clicked()
{
    // 如果已经在录制,先停止之前的
    if (m_recorder) {
        m_recorder->stopRecording();
        if (m_timer) {
            m_timer->stop();
        }
        m_recorder->deleteLater();
        m_recorder = nullptr;
    }

    videoRecorder();
}

void Widget::onTimeout_10s()
{
    if (!m_recorder) {
        return;
    }

    // 创建测试图片(模拟摄像头数据)
    QImage image(1920, 1080, QImage::Format_RGB32);

    // 创建渐变效果,更容易看出视频是否正常
    for (int y = 0; y < image.height(); ++y) {
        QRgb* line = reinterpret_cast<QRgb*>(image.scanLine(y));
        int r = (y + m_frameCount) % 255;
        int g = (m_frameCount * 2) % 255;
        int b = (y * 2 + m_frameCount) % 255;
        for (int x = 0; x < image.width(); ++x) {
            line[x] = qRgb(r, g, b);
        }
    }
    //测试 addFrame 输入
    QByteArray imageData(reinterpret_cast<const char*>(image.bits()),
                         image.sizeInBytes());

    // 添加帧到录制器
    m_recorder->addFrame(image);
    m_frameCount++;

    // 显示进度(每30帧显示一次)
    if (m_frameCount % 30 == 0) {
        qDebug() << "Added frame:" << m_frameCount;
    }

    // 录制300帧后自动停止(约10秒,30fps)
    if (m_frameCount >= 300) {
        qDebug() << "Auto stop after 300 frames";
        m_recorder->stopRecording();
        if (m_timer) {
            m_timer->stop();
        }
    }
}

void Widget::onTimeout_noRestrictions()
{
    if (!m_recorder) {
        return;
    }

    // 创建测试图片(模拟摄像头数据)
    QImage image(1920, 1080, QImage::Format_RGB32);

    // 创建渐变效果,更容易看出视频是否正常
    for (int y = 0; y < image.height(); ++y) {
        QRgb* line = reinterpret_cast<QRgb*>(image.scanLine(y));
        int r = (y + m_frameCount) % 255;
        int g = (m_frameCount * 2) % 255;
        int b = (y * 2 + m_frameCount) % 255;
        for (int x = 0; x < image.width(); ++x) {
            line[x] = qRgb(r, g, b);
        }
    }
    //测试 addFrame 输入
    QByteArray imageData(reinterpret_cast<const char*>(image.bits()),
                         image.sizeInBytes());

    // 添加帧到录制器
    m_recorder->addFrame(image);
    m_frameCount++;

    // 显示进度(每30帧显示一次)
    if (m_frameCount % 30 == 0) {
        qDebug() << "Added frame:" << m_frameCount;
    }
}

void Widget::onRecordingStopped(bool success)
{
    qDebug() << "Recording stopped signal received, success:" << success;

    if (m_timer) {
        m_timer->stop();
    }

    // 延迟删除录制器
    if (m_recorder) {
        m_recorder->deleteLater();
        m_recorder = nullptr;
    }
}

void Widget::videoRecorder()
{
    qDebug() << "Starting video recorder...";

    // 创建录制器
    m_recorder = new VideoRecorder(this);

    // 设置视频参数
    m_recorder->setVideoParameters(1920, 1080, 30, 2000000);

    // 连接信号槽
    connect(m_recorder, &VideoRecorder::recordingStarted, this, []() {
        qDebug() << "Recording started!";
    });

    connect(m_recorder, &VideoRecorder::recordingStopped, this, &Widget::onRecordingStopped);

    connect(m_recorder, &VideoRecorder::errorOccurred, this, [](const QString& error) {
        qDebug() << "Error:" << error;
    });

    connect(m_recorder, &VideoRecorder::frameEncoded, this, [](int count) {
        // 每30帧输出一次日志,减少刷屏
        static int lastLogCount = 0;
        if (count - lastLogCount >= 30) {
            qDebug() << "Encoded frame count:" << count;
            lastLogCount = count;
        }
    });

    // 开始录制
    if (!m_recorder->startRecording("output.mp4")) {
        qDebug() << "Failed to start recording";
        m_recorder->deleteLater();
        m_recorder = nullptr;
        return;
    }

    // 开始模拟数据源
    m_frameCount = 0;
    connect(m_timer, &QTimer::timeout, this, &Widget::onTimeout_10s);
    m_timer->start(33);  // 约30fps (1000/30 ≈ 33ms)

    qDebug() << "Timer started, will generate frames every 33ms";
}

void Widget::on_pushButton_2_clicked()
{
    static int m_clicked = 0;
    if(((m_clicked++) % 2) == 0){
        qDebug()<<"start";
        ui->pushButton_2->setText(QString::fromLocal8Bit("停止录制-不限时"));

        // 如果已经在录制,先停止之前的
        if (m_recorder) {
            m_recorder->stopRecording();
            if (m_timer) {
                m_timer->stop();
            }
            m_recorder->deleteLater();
            m_recorder = nullptr;
        }

        // 创建录制器
        m_recorder = new VideoRecorder(this);

        qDebug() << "Starting video recorder...";

        // 设置视频参数
        m_recorder->setVideoParameters(1920, 1080, 30, 2000000);

        // 连接信号槽
        connect(m_recorder, &VideoRecorder::recordingStarted, this, []() {
            qDebug() << "Recording started!";
        });

        connect(m_recorder, &VideoRecorder::errorOccurred, this, [](const QString& error) {
            qDebug() << "Error:" << error;
        });

        connect(m_recorder, &VideoRecorder::frameEncoded, this, [](int count) {
            // 每30帧输出一次日志,减少刷屏
            static int lastLogCount = 0;
            if (count - lastLogCount >= 30) {
                qDebug() << "Encoded frame count:" << count;
                lastLogCount = count;
            }
        });

        // 开始录制
        if (!m_recorder->startRecording("output.mp4")) {
            qDebug() << "Failed to start recording";
            m_recorder->deleteLater();
            m_recorder = nullptr;
            return;
        }

        // 开始模拟数据源
        m_frameCount = 0;
        connect(m_timer, &QTimer::timeout, this, &Widget::onTimeout_noRestrictions);
        m_timer->start(33);  // 约30fps (1000/30 ≈ 33ms)

        qDebug() << "Timer started, will generate frames every 33ms";
    }else{
        qDebug()<<"stop";
        ui->pushButton_2->setText(QString::fromLocal8Bit("开始录制-不限时"));

        m_recorder->stopRecording();

        if (m_timer) {
            m_timer->stop();
        }

        // 延迟删除录制器
        if (m_recorder) {
            m_recorder->deleteLater();
            m_recorder = nullptr;
        }
    }
}
相关推荐
csbysj20208 小时前
框架:构建高效解决方案的基石
开发语言
轻颂呀9 小时前
C++11——并发库介绍
开发语言·c++
AKA__Zas9 小时前
初识多线程(3.0)
java·开发语言·学习方法
小杍随笔9 小时前
【Rust 工具链管理工具再升级!rust-verse v1.3.1 ~ v1.3.5 最新更新深度解析】
开发语言·后端·rust
福老板的生意经10 小时前
AI 短视频全链路创作分发系统架构解析:模块化设计与核心技术实现
人工智能·系统架构·音视频
大数据三康10 小时前
在spyder进行的遗传算法练习
开发语言·python·算法
Vallelonga10 小时前
Rust 从结构体中取字段的引用
开发语言·rust
社交怪人10 小时前
【球体体积】信息学奥赛一本通C语言解法(题号1030)
c语言·开发语言
cpp_learners10 小时前
QT 窗体遮罩
qt·遮罩