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

一个简单的Rtmp推流客户端(QT录音,OpenCV摄像,FFmpeg编码推流)

        RTMP(Real-Time Messaging Protocol)是一种实时流媒体传输协议,常用于音视频直播。

        RTMP推流客户端是一种能够将音视频数据推送到直播服务器的工具。QT录音是利用Qt库实现的录音功能。OpenCV摄像是利用OpenCV库实现的对摄像头的控制和图像处理功能。FFmpeg编码推流是利用FFmpeg库实现的将音视频数据进行编码并推流到RTMP服务器的功能。

        在本文中,我们将介绍如何使用RTMP推流客户端结合QT录音、OpenCV摄像和FFmpeg编码推流来实现将音视频数据推送到RTMP服务器的功能。

一、 环境介绍    

1、QT版本: QT5.12.12

2、编译器:  MSVC2017 64

3、ffmpeg版本: 6.1.1

4、openCV 4.x

5、完整工程下载地址(下载即可编译运行): https://download.csdn.net/download/u012959478/89646684

二、实现思路  

这个推流客户端的主要运行有三个线程,并且需要两个队列。

线程1(音频数据采集):使用QT录音功能,将音频数据采集下来,并存入音频队列中。

线程2(视频数据采集):使用OpenCV库实现视频数据的采集,将摄像头捕获的视频数据存入视频队列中。

线程3(推流):从音频和视频队列中读取数据,并使用FFmpeg库对音视频数据进行编码,最终推流到指定的服务器。

三、示例代码  
 AVFrameQueue.h
#pragma onceextern "C" {
#include "libavcodec/avcodec.h"
}#include <QQueue>
#include <QMutex>class AVFrameQueue
{
public:// 添加元素到队列尾部void enqueue(AVFrame* value) {QMutexLocker locker(&m_mutex);AVFrame* tmp_frame = av_frame_alloc();av_frame_move_ref(tmp_frame, value);m_queue.enqueue(tmp_frame);}// 从队列头部移除一个元素,并返回它AVFrame* dequeue() {QMutexLocker locker(&m_mutex);if (m_queue.isEmpty()) {return nullptr;}return m_queue.dequeue();}// 返回队列是否为空bool isEmpty() const {QMutexLocker locker(&m_mutex);return m_queue.isEmpty();}int size() const {QMutexLocker locker(&m_mutex);return m_queue.size();}void clear() {QMutexLocker locker(&m_mutex);m_queue.clear();}private:QQueue<AVFrame*> m_queue;mutable QMutex m_mutex;
};
audiorecordthread.h音频采集线程
#ifndef AUDIORECORDTHREAD_H
#define AUDIORECORDTHREAD_H#include <QThread>
#include <QAudioInput>
#include "AVFrameQueue.h"extern "C"
{
#include <libswresample/swresample.h>
#include <libavformat/avformat.h>
}class AudioRecordThread : public QThread
{Q_OBJECT
public:explicit AudioRecordThread(AVFrameQueue * frame_queue);~AudioRecordThread();bool Init();private:void run();bool InitResample();void increaseVolume(AVFrame *frame, double volume);//提高音量private:SwrContext *_swr_ctx = nullptr;AVFrame* _pcmAvFrame = nullptr;QAudioInput *_input = nullptr;QIODevice *_io = nullptr;AVFrameQueue *_frame_queue = nullptr;int channels = 2; // 声道数int sampleRate = 44100; // 采样率int sampleByte = 2; // 采样字节数(2字节,16位)int nbSamples = 1024; // 一帧音频每个通道的采样数量
};#endif // AUDIORECORDTHREAD_H
audiorecordthread.cpp 
#include "audiorecordthread.h"
#include <QDebug>AudioRecordThread::AudioRecordThread(AVFrameQueue * frame_queue):_frame_queue(frame_queue)
{connect(this, &AudioRecordThread::finished,this, &AudioRecordThread::deleteLater);
}AudioRecordThread::~AudioRecordThread()
{requestInterruption();if (_input)_input->stop();_input = nullptr;if (_io)_io->close();   _io = nullptr;swr_free(&_swr_ctx);av_frame_free(&_pcmAvFrame);quit();wait();qDebug() << "AudioRecordThread析构";
}bool AudioRecordThread::Init()
{if(QAudioDeviceInfo::availableDevices(QAudio::AudioInput).size()<1){qDebug()<<"没有录音设备";return false;}QAudioFormat fmt;fmt.setSampleRate(sampleRate);fmt.setChannelCount(channels);fmt.setSampleSize(sampleByte * 8);fmt.setCodec("audio/pcm");fmt.setByteOrder(QAudioFormat::LittleEndian);fmt.setSampleType(QAudioFormat::UnSignedInt);QAudioDeviceInfo info = QAudioDeviceInfo::defaultInputDevice();if (!info.isFormatSupported(fmt)) {fmt = info.nearestFormat(fmt);}_input = new QAudioInput(fmt);//开始录制音频_io = _input->start();if (!_io)return false;if(!InitResample())return false;return true;
}bool AudioRecordThread::InitResample()
{// 音频重采样 上下文初始化_swr_ctx = swr_alloc_set_opts(nullptr,av_get_default_channel_layout(channels), AV_SAMPLE_FMT_S16, sampleRate,//输出格式av_get_default_channel_layout(channels), AV_SAMPLE_FMT_S16, sampleRate, 0, nullptr);//输入格式if (!_swr_ctx){return false;}int ret = swr_init(_swr_ctx);if (ret < 0){return false;}return true;
}void AudioRecordThread::run()
{int readSize = nbSamples * channels * sampleByte;char* buf = new char[readSize];while(!isInterruptionRequested()){if (_frame_queue->size() > 10) {msleep(10);continue;}//一次读取一帧音频if (_input->bytesReady() < readSize){QThread::msleep(1);continue;}int size = 0;while (size != readSize){int len = _io->read(buf + size, readSize - size);if (len < 0)break;size += len;}if (size != readSize)continue;//已经读一帧源数据const uint8_t *indata[AV_NUM_DATA_POINTERS] = { 0 };indata[0] = (uint8_t *)buf;//音频重采样输出空间分配_pcmAvFrame = av_frame_alloc();_pcmAvFrame->format = AV_SAMPLE_FMT_S16;_pcmAvFrame->channels = channels;_pcmAvFrame->channel_layout = av_get_default_channel_layout(channels);_pcmAvFrame->nb_samples = nbSamples; //一帧音频一通道的采用数量av_frame_get_buffer(_pcmAvFrame, 0); // 给pcm分配存储空间swr_convert(_swr_ctx, _pcmAvFrame->data, _pcmAvFrame->nb_samples, indata, nbSamples);increaseVolume(_pcmAvFrame,8);//简单的提高音量,没有回声消除,噪音抑制_frame_queue->enqueue(_pcmAvFrame);msleep(1);}delete []buf;
}void AudioRecordThread::increaseVolume(AVFrame *frame, double volume)
{int16_t *samples = (int16_t *)frame->data[0];int nb_samples = frame->nb_samples;int channels = av_get_channel_layout_nb_channels(frame->channel_layout);// 提高音量for (int i = 0; i < nb_samples; i++){for (int ch = 0; ch < channels; ch++){// 使用线性插值来提高音量int pcmval = samples[ch] * volume;if (pcmval < 32767 && pcmval > -32768){samples[ch] = pcmval;}else if (pcmval > 32767){samples[ch] = 32767;}else if (pcmval < -32768){samples[ch] = -32768;}}samples += channels;}
}
videocapturethread.h视频采集线程
#ifndef VIDEOCAPTURETHREAD_H
#define VIDEOCAPTURETHREAD_H#include <QThread>
#include "AVFrameQueue.h"
#include "opencv2/opencv.hpp"extern "C"
{
#include <libswscale/swscale.h>
#include <libswresample/swresample.h>
}class VideoCaptureThread : public QThread
{Q_OBJECT
public:explicit VideoCaptureThread(AVFrameQueue * frame_queue);~VideoCaptureThread();bool Init(int camIndex = 0); // 打开本地摄像头bool Init(const char* url); // 打开流bool InitScale();private:void run();AVFrame* RGBToYUV(cv::Mat &frame);private:AVFrameQueue *_frame_queue = nullptr;cv::VideoCapture capture;SwsContext* _swsContext = nullptr; // 像素格式转换上下文AVFrame* _yuvAvFrame = nullptr; // 存放转换后的YUV数据int inWidth;int inHeight;
//    int fps;int outWidth = 640;int outHeight = 360;
};#endif // VIDEOCAPTURETHREAD_H
videocapturethread.cpp
#include "videocapturethread.h"
#include <QDebug>VideoCaptureThread::VideoCaptureThread(AVFrameQueue * frame_queue) : _frame_queue(frame_queue)
{connect(this, &VideoCaptureThread::finished,this, &VideoCaptureThread::deleteLater);
}VideoCaptureThread::~VideoCaptureThread()
{requestInterruption();if (capture.isOpened()){capture.release();}sws_freeContext(_swsContext);av_frame_free(&_yuvAvFrame);quit();wait();qDebug() << "VideoCaptureThread析构";
}bool VideoCaptureThread::Init(int camIndex)
{// 打开本地摄像头capture.open(camIndex);if (!capture.isOpened()){return false;}// 得到本地相机参数inWidth = capture.get(cv::CAP_PROP_FRAME_WIDTH);inHeight = capture.get(cv::CAP_PROP_FRAME_HEIGHT);
//    fps = capture.get(cv::CAP_PROP_FPS);return true;
}bool VideoCaptureThread::Init(const char *url)
{capture.open(url);if (!capture.isOpened()){return false;}// 得到流媒体的参数inWidth = capture.get(cv::CAP_PROP_FRAME_WIDTH);inHeight = capture.get(cv::CAP_PROP_FRAME_HEIGHT);
//    fps = capture.get(cv::CAP_PROP_FPS);return true;
}bool VideoCaptureThread::InitScale()
{_swsContext = sws_getCachedContext(_swsContext,inWidth, inHeight, AV_PIX_FMT_BGR24,outWidth, outHeight, AV_PIX_FMT_YUV420P,SWS_BICUBIC,0, 0, 0);if (!_swsContext){return false;}return true;
}void VideoCaptureThread::run()
{cv::Mat frame;while(!isInterruptionRequested()){if (_frame_queue->size() > 10) {msleep(10);continue;}// 读取一帧if (!capture.read(frame)) {msleep(1); // 如果没有读取到,等待1mscontinue;}AVFrame *yuv = RGBToYUV(frame);_frame_queue->enqueue(yuv);msleep(1);}
}AVFrame* VideoCaptureThread::RGBToYUV(cv::Mat &frame)
{//输入的数据结构uint8_t *indata[AV_NUM_DATA_POINTERS] = {0};indata[0] = frame.data;int insize[AV_NUM_DATA_POINTERS] = {0};// 一行(宽)数据的字节数insize[0] = frame.cols * frame.elemSize();_yuvAvFrame = av_frame_alloc();_yuvAvFrame->format = AV_PIX_FMT_YUV420P;_yuvAvFrame->width = outWidth;_yuvAvFrame->height = outHeight;_yuvAvFrame->pts = 0;// 实际分配yuv空间int ret = av_frame_get_buffer(_yuvAvFrame, 0);if (ret != 0){return nullptr;}// 开始格式转换,把转换后的数据存放到yuvAvFrame->data中int h = sws_scale(_swsContext, indata, insize, 0, frame.rows,_yuvAvFrame->data, _yuvAvFrame->linesize);if (h <= 0){return nullptr;}return _yuvAvFrame;
}
 mediaencode.h
#ifndef MEDIAENCODE_H
#define MEDIAENCODE_H#include <QObject>extern "C"
{
#include <libavcodec/avcodec.h>
}class MediaEncode : public QObject
{Q_OBJECT
public:explicit MediaEncode(QObject *parent = nullptr);~MediaEncode();bool InitVideoCodec();// 视频编码器初始化AVPacket* EncodeVideo(AVFrame* frame);// 开始编码视频bool InitAudioCodec();// 音频编码器初始化AVPacket* EncodeAudio(AVFrame* frame);// 开始音频编码public:// 视频编码器上下文, YUV->H264AVCodecContext* _videoCodecContext = nullptr;// 音频编码上下文, PCM-AACAVCodecContext* _audioCodecContext = nullptr;private:int outWidth = 640; //和采集的尺寸保持一致int outHeight = 360;int fps = 30;int videoPts = 0;int audioPts = 0;AVPacket outAudioPacket = {0};AVPacket outVideoPacket = {0};
};#endif // MEDIAENCODE_H
mediaencode.cpp
#include "mediaencode.h"MediaEncode::MediaEncode(QObject *parent) : QObject(parent)
{InitVideoCodec();InitAudioCodec();
}MediaEncode::~MediaEncode()
{avcodec_free_context(&_videoCodecContext);avcodec_free_context(&_audioCodecContext);
}bool MediaEncode::InitVideoCodec()
{int ret = 0;// 找到编码器const AVCodec* videoCodec = avcodec_find_encoder(AV_CODEC_ID_H264);if (!videoCodec) {return false;}// 创建编码器上下文_videoCodecContext = avcodec_alloc_context3(videoCodec);if (!_videoCodecContext) {return false;}// 配置编码器参数_videoCodecContext->flags |= AV_CODEC_FLAG_GLOBAL_HEADER;_videoCodecContext->codec_id = videoCodec->id;
//    _videoCodecContext->thread_count = 8;//压缩后每秒视频的bit位大小_videoCodecContext->bit_rate = 1200 * 1024;_videoCodecContext->width = outWidth;_videoCodecContext->height = outHeight;_videoCodecContext->time_base = {1, fps};_videoCodecContext->framerate = {fps, 1};// 画面组的大小,多少帧一个关键帧_videoCodecContext->gop_size = 15;_videoCodecContext->max_b_frames = 0;_videoCodecContext->pix_fmt = AV_PIX_FMT_YUV420P;// 打开编码器上下文ret = avcodec_open2(_videoCodecContext, 0, 0);if (ret != 0) {return false;}return true;
}AVPacket* MediaEncode::EncodeVideo(AVFrame* frame)
{// 开始h264编码, pts必须递增frame->pts = videoPts;videoPts++;// 发送原始帧,开始编码int ret = avcodec_send_frame(_videoCodecContext, frame);if (ret != 0) {return nullptr;}av_packet_unref(&outVideoPacket);ret = avcodec_receive_packet(_videoCodecContext, &outVideoPacket);if (ret != 0 || outVideoPacket.size <= 0) {return nullptr;}return &outVideoPacket;
}bool MediaEncode::InitAudioCodec()
{const AVCodec *codec = avcodec_find_encoder_by_name("libfdk_aac");if(!codec){return false;}_audioCodecContext = avcodec_alloc_context3(codec);if (!_audioCodecContext) {return false;}_audioCodecContext->sample_fmt = AV_SAMPLE_FMT_S16;       // 输入音频的采样大小。fdk_aac需要16位的音频输													                入数据_audioCodecContext->channel_layout = AV_CH_LAYOUT_STEREO; // 输入音频的CHANNEL LAYOUT_audioCodecContext->channels = 2;                         // 输入音频的声道数_audioCodecContext->sample_rate = 44100;                  // 输入音频的采样率_audioCodecContext->bit_rate = 0;                         // AAC : 128K   AAV_HE: 64K  AAC_HE_V2: 32K. bit_rate为0时会查找profile属性值
//    _audioCodecContext->thread_count = 8;// 打开编码器int ret = avcodec_open2(_audioCodecContext,codec,nullptr);if (ret < 0) {return false;}return true;
}AVPacket * MediaEncode::EncodeAudio(AVFrame* frame)
{frame->pts = audioPts;audioPts += av_rescale_q(frame->nb_samples, { 1, 44100 }, _audioCodecContext->time_base);int ret = avcodec_send_frame(_audioCodecContext, frame);if (ret != 0)return nullptr;av_packet_unref(&outAudioPacket);ret = avcodec_receive_packet(_audioCodecContext, &outAudioPacket);if (ret != 0)return nullptr;return &outAudioPacket;
}
rtmppushthread.h推流线程
#ifndef RTMPPUSHTHREAD_H
#define RTMPPUSHTHREAD_H#include <QThread>
#include "AVFrameQueue.h"
#include "mediaencode.h"
extern "C"
{
#include <libavformat/avformat.h>
}class RtmpPushThread : public QThread
{Q_OBJECT
public:explicit RtmpPushThread(AVFrameQueue *audioFrameQueue,AVFrameQueue *videoFrameQueue,QObject *parent = nullptr);~RtmpPushThread();bool InitMux(const char* url);private:void run();// 添加视频或者音频流int AddStream(const AVCodecContext* codecContext);// 打开RTMP网络IO,发送封装头MUXbool SendMuxHead();// RTMP推流bool SendFrame(AVPacket* pack, int streamIndex);private:AVFrameQueue *_audioFrameQueue = nullptr;AVFrameQueue *_videoFrameQueue = nullptr;MediaEncode *_mediaEncode = nullptr;AVFormatContext* _avFormatContext = nullptr;//FLV 封装器const AVCodecContext *_videoCodecContext = nullptr;const AVCodecContext *_audioCodecContext = nullptr;AVStream *_videoStream = nullptr;AVStream *_audioStream = nullptr;std::string outURL = "";
};#endif // RTMPPUSHTHREAD_H
rtmppushthread.cpp
#include "rtmppushthread.h"
#include <QDebug>RtmpPushThread::RtmpPushThread(AVFrameQueue *audioFrameQueue,AVFrameQueue *videoFrameQueue,QObject *parent): QThread(parent),_audioFrameQueue(audioFrameQueue),_videoFrameQueue(videoFrameQueue)
{connect(this, &RtmpPushThread::finished,this, &RtmpPushThread::deleteLater);_mediaEncode = new MediaEncode(this);
}RtmpPushThread::~RtmpPushThread()
{requestInterruption();if (_avFormatContext){avformat_close_input(&_avFormatContext);_avFormatContext = nullptr;}quit();wait();qDebug() << "RtmpPushThread析构";
}bool RtmpPushThread::InitMux(const char* url)
{int ret = avformat_alloc_output_context2(&_avFormatContext, 0, "flv", url);outURL = url;if (ret != 0) {return false;}return true;
}void RtmpPushThread::run()
{int aindex = AddStream(_mediaEncode->_audioCodecContext);int vindex = AddStream(_mediaEncode->_videoCodecContext);if(!SendMuxHead())return;while(!isInterruptionRequested()){AVFrame *audioFrame = _audioFrameQueue->dequeue();AVFrame *videoFrame = _videoFrameQueue->dequeue();if (audioFrame == nullptr && videoFrame == nullptr){msleep(1);continue;}//处理音频if (audioFrame){AVPacket *pkt = _mediaEncode->EncodeAudio(audioFrame);if (pkt){SendFrame(pkt,aindex); //推流}av_frame_free(&audioFrame);}//处理视频if (videoFrame){AVPacket *pkt = _mediaEncode->EncodeVideo(videoFrame);if (pkt){SendFrame(pkt,vindex); //推流}av_frame_free(&videoFrame);}msleep(1);}
}int RtmpPushThread::AddStream(const AVCodecContext* codecContext) {if (!codecContext) {return -1;}// 添加视频流AVStream* avStream = avformat_new_stream(_avFormatContext, NULL);if (!avStream) {return -1;}avStream->codecpar->codec_tag = 0;// 从编码器复制参数avcodec_parameters_from_context(avStream->codecpar, codecContext);av_dump_format(_avFormatContext, 0, outURL.c_str(), 1);if (codecContext->codec_type == AVMEDIA_TYPE_VIDEO) {_videoCodecContext = codecContext;_videoStream = avStream;}else if (codecContext->codec_type == AVMEDIA_TYPE_AUDIO) {_audioCodecContext = codecContext;_audioStream = avStream;}return avStream->index;
}// 打开RTMP网络IO,发送封装头MUX
bool RtmpPushThread::SendMuxHead() {///打开rtmp 的网络输出IOint ret = avio_open(&_avFormatContext->pb, outURL.c_str(), AVIO_FLAG_WRITE);if (ret != 0){return false;}//写入封装头ret = avformat_write_header(_avFormatContext, NULL);if (ret != 0){return false;}return true;
}bool RtmpPushThread::SendFrame(AVPacket* pack, int streamIndex)
{if (!pack || pack->size <= 0 || !pack->data)return false;pack->stream_index = streamIndex;AVRational stime;AVRational dtime;//判断是音频还是视频if (_videoStream && _videoCodecContext && pack->stream_index == _videoStream->index){stime = _videoCodecContext->time_base;dtime = _videoStream->time_base;}else if (_audioStream && _audioCodecContext &&pack->stream_index == _audioStream->index){stime = _audioCodecContext->time_base;dtime = _audioStream->time_base;}else{return false;}//推流pack->pts = av_rescale_q(pack->pts, stime, dtime);pack->dts = av_rescale_q(pack->dts, stime, dtime);pack->duration = av_rescale_q(pack->duration, stime, dtime);int ret = av_interleaved_write_frame(_avFormatContext, pack);if (ret == 0){return true;}return false;
}
 界面设计mainwindow.ui

mainwindow.h
#ifndef MAINWINDOW_H
#define MAINWINDOW_H#include <QMainWindow>
#include "rtmppushthread.h"
#include "audiorecordthread.h"
#include "videocapturethread.h"QT_BEGIN_NAMESPACE
namespace Ui { class MainWindow; }
QT_END_NAMESPACEclass MainWindow : public QMainWindow
{Q_OBJECTpublic:MainWindow(QWidget *parent = nullptr);~MainWindow();private slots:void on_pushButton_clicked();void onPushThreadFinished();private:Ui::MainWindow *ui;AVFrameQueue audioFrameQueue;AVFrameQueue videoFrameQueue;RtmpPushThread *_pushThread = nullptr;AudioRecordThread *_audioThread = nullptr;VideoCaptureThread *_videoThread = nullptr;
};
#endif // MAINWINDOW_H
mainwindow.cpp
#include "mainwindow.h"
#include "ui_mainwindow.h"MainWindow::MainWindow(QWidget *parent): QMainWindow(parent), ui(new Ui::MainWindow)
{ui->setupUi(this);ui->lineEdit->setText("rtmp://192.168.37.128/live/livestream");avformat_network_init();
}MainWindow::~MainWindow()
{delete ui;
}void MainWindow::on_pushButton_clicked()
{if(!_pushThread){_audioThread = new AudioRecordThread(&audioFrameQueue);if(!_audioThread->Init())return;_videoThread = new VideoCaptureThread(&videoFrameQueue);if(!_videoThread->Init() || !_videoThread->InitScale())return;_pushThread = new RtmpPushThread(&audioFrameQueue,&videoFrameQueue,this);connect(_pushThread,&RtmpPushThread::finished,this,&MainWindow::onPushThreadFinished);if(!_pushThread->InitMux(ui->lineEdit->text().toUtf8().data()))return;_audioThread->start();_videoThread->start();_pushThread->start();ui->pushButton->setText("停止推流");}else{_audioThread->requestInterruption();_videoThread->requestInterruption();_pushThread->requestInterruption();}
}void MainWindow::onPushThreadFinished()
{_pushThread = nullptr;_audioThread = nullptr;_videoThread = nullptr;audioFrameQueue.clear();videoFrameQueue.clear();ui->pushButton->setText("开始推流");
}

        以上介绍了如何使用RTMP推流客户端结合QT录音、OpenCV摄像和FFmpeg编码推流来实现将音视频数据推送到RTMP服务器的功能。通过这种方式,我们可以实现将音视频数据实时推送到RTMP服务器。

        使用RTMP推流客户端结合QT录音、OpenCV摄像和FFmpeg编码推流,可以实现许多应用场景,如实时直播、视频会议、监控系统等。

四、运行效果

1、启动自己搭建的SRS服务器,详情请见:Ubuntu24.04使用SRS 搭建 RTMP流媒体服务器-CSDN博客

2、运行程序开始推流 

        以上就是启动SRS服务器并开始推流的流程。请根据实际情况调整步骤中的路径和参数,并参考具体的SRS搭建指南进行操作。

        这是一个简单的Rtmp推流客户端示例,使用QT进行音频录制、OpenCV进行摄像、FFmpeg进行编码和推流。请注意,这只是一个简单的示例代码,实际的RTMP推流客户端可能需要更多的功能和错误处理。您可以根据自己的需求进行修改和扩展。

        谢谢您的阅读。希望本文能对您有所帮助,并且给您带来了一些新的观点和思考。如果您有任何问题或意见,请随时与我联系。再次感谢您的支持!

 五、相关文章

Windosw下Visual Studio2022编译FFmpeg(支持x264、x265、fdk-acc)-CSDN博客

Windosw下Visual Studio2022编译OpenCV-CSDN博客

相关文章:

  • 北京网站建设多少钱?
  • 辽宁网页制作哪家好_网站建设
  • 高端品牌网站建设_汉中网站制作
  • 模糊Smith智能控制方法的研究 及其单片机实现
  • 打造聊天流式回复效果:Spring Boot+WebSocket + JS实战
  • Python办公自动化:初识 `openpyxl`
  • Ubuntu离线安装库并解决依赖关系
  • 发信息(c语言)
  • golang提案,内置 Go 错误检查函数
  • 【前端】NodeJS:记账本案例优化(token)
  • leetCode - - - 双指针
  • 解密JVM崩溃(Crash)-学习笔记
  • qt-12工具盒(ToolBox)
  • 数学基础 -- 指数增长与指数衰变
  • 使用Go语言将PDF文件转换为Base64编码
  • Wireshark分析工具
  • 构建艺术:Ruby中RESTful API的精粹实践
  • 【IDEA】idea配置服务器没有tomcat
  • 【React系列】如何构建React应用程序
  • canvas绘制圆角头像
  • JavaScript DOM 10 - 滚动
  • Linux CTF 逆向入门
  • Spring Cloud中负载均衡器概览
  • SQLServer之索引简介
  • vue学习系列(二)vue-cli
  • 测试开发系类之接口自动化测试
  • 彻底搞懂浏览器Event-loop
  • 分布式任务队列Celery
  • 湖南卫视:中国白领因网络偷菜成当代最寂寞的人?
  • 开年巨制!千人千面回放技术让你“看到”Flutter用户侧问题
  • 前端每日实战 2018 年 7 月份项目汇总(共 29 个项目)
  • 再谈express与koa的对比
  • 组复制官方翻译九、Group Replication Technical Details
  • $(document).ready(function(){}), $().ready(function(){})和$(function(){})三者区别
  • (20)目标检测算法之YOLOv5计算预选框、详解anchor计算
  • (CPU/GPU)粒子继承贴图颜色发射
  • (PyTorch)TCN和RNN/LSTM/GRU结合实现时间序列预测
  • (附源码)php新闻发布平台 毕业设计 141646
  • (规划)24届春招和25届暑假实习路线准备规划
  • (四)事件系统
  • (推荐)叮当——中文语音对话机器人
  • (一)WLAN定义和基本架构转
  • (原創) 未来三学期想要修的课 (日記)
  • (转)Oracle 9i 数据库设计指引全集(1)
  • (转)项目管理杂谈-我所期望的新人
  • (转载)hibernate缓存
  • .bat批处理(一):@echo off
  • .Net core 6.0 升8.0
  • .NET Standard 支持的 .NET Framework 和 .NET Core
  • .NET WPF 抖动动画
  • .NET/ASP.NETMVC 深入剖析 Model元数据、HtmlHelper、自定义模板、模板的装饰者模式(二)...
  • .Net多线程总结
  • @JsonFormat 和 @DateTimeFormat 的区别
  • []串口通信 零星笔记
  • [23] GaussianAvatars: Photorealistic Head Avatars with Rigged 3D Gaussians
  • [2544]最短路 (两种算法)(HDU)
  • [51nod1610]路径计数
  • [Android] Android ActivityManager