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

news2024/11/16 5:56:28

        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 once

extern "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); // 如果没有读取到,等待1ms
            continue;
        }

        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->H264
    AVCodecContext* _videoCodecContext = nullptr;
    // 音频编码上下文, PCM-AAC
    AVCodecContext* _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,发送封装头MUX
    bool 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 的网络输出IO
    int 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_NAMESPACE

class MainWindow : public QMainWindow
{
    Q_OBJECT

public:
    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博客

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2049978.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Chrome书签搜索插件

效果展示 这是一个chroma插件&#xff0c;可以按住 ctrl/command B 进行搜索您的书签&#xff0c;并且点击打开您的书签。支持上下切换回车打开新页面。支持文件夹搜索。多层级文件夹使用 / 分割。如&#xff1a;文件夹1/文件夹2/标签1 扩展下载地址 bookmark-search 欢迎有…

小程序学习day09-WXS脚本、自定义组件-组件的创建、引用、组件与页面的区别、组件的样式隔离

39、WXS脚本&#xff08;小程序独有的一套脚本语言&#xff09; &#xff08;1&#xff09;作用&#xff1a;结合WXML&#xff0c;可以构建出页面结构 &#xff08;2&#xff09;应用&#xff1a;在小程序中充当过滤器。&#xff08;wxml无法调用在页面的.js中定义的函数&…

DNS in Kubernetes

DNS in Kubernetes 对象分配的名称Service DNS 记录Pod DNS 记录 Cluster DNS参考 DNS for Services and Pods 这里主要讨论集群内不同对象之间的DNS解析 默认情况下&#xff0c;创建集群时&#xff0c;k8s会部署内置的DNS服务器&#xff0c;在集群内&#xff0c;我们不关注…

spring-boot-3.2.6+spring-security-6.2.4+oauth2整合github示例

一、添加依赖 在 pom.xml 中添加如下依赖 <?xml version"1.0" encoding"UTF-8"?> <project xmlns"http://maven.apache.org/POM/4.0.0" xmlns:xsi"http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation"h…

<数据集>航拍路面病害识别数据集<目标检测>

数据集格式&#xff1a;VOCYOLO格式 图片数量&#xff1a;3151张 标注数量(xml文件个数)&#xff1a;3151 标注数量(txt文件个数)&#xff1a;3151 标注类别数&#xff1a;7 标注类别名称&#xff1a;[Longitudinal crack, Transverse crack, Alligator crack, Oblique cr…

腾讯 图标点选 分析

声明: 本文章中所有内容仅供学习交流使用&#xff0c;不用于其他任何目的&#xff0c;抓包内容、敏感网址、数据接口等均已做脱敏处理&#xff0c;严禁用于商业用途和非法用途&#xff0c;否则由此产生的一切后果均与作者无关&#xff01; 有相关问题请第一时间头像私信联系我…

Element UI中报dateObject.getTime is not a function解决方法~

1、错误信息。 2、该报错原因是Element UI中日期组件的校验规则是type: "date",而一般我们从后台拿到的数据是字符串型的&#xff0c;不满足预期&#xff0c;就会报错。 3、解决方法。 去掉日子组件中的type: "date"校验规则即可。 rules: {newName: [{…

SOMEIP_ETS_046: echoUTF16FIXED

测试目的&#xff1a; 验证设备&#xff08;DUT&#xff09;是否能够正确处理echoUTF16FIXED方法的参数&#xff0c;并确保发送和接收的参数顺序和值保持一致。 描述 本测试用例旨在检查DUT在接收到一个使用echoUTF16FIXED方法的SOME/IP消息时&#xff0c;是否能够按照请求中…

利用PostgreSQL向量数据库和负责任的AI知识库在亚马逊云科技上构建商品推荐机器人

项目简介&#xff1a; 小李哥将继续每天介绍一个基于亚马逊云科技AWS云计算平台的全球前沿AI技术解决方案&#xff0c;帮助大家快速了解国际上最热门的云计算平台亚马逊云科技AWS AI最佳实践&#xff0c;并应用到自己的日常工作里。 本次介绍的是如何在亚马逊云科技利用Postg…

软件需求规格说明书编写规范(Doc原件)

软件需求规格说明书编写规范&#xff08;Word原件&#xff09; 1.项目背景 2.项目目标 3.系统架构 4.总体流程 5.名称解释 6.功能模块 软件全套资料部分文档清单&#xff1a; 工作安排任务书&#xff0c;可行性分析报告&#xff0c;立项申请审批表&#xff0c;产品需求规格说明…

WPF MvvmLight

关于 停止更新 官网&#xff1a;http://www.mvvmlight.net/ 源码地址&#xff1a;GitHub - lbugnion/mvvmlight: The main purpose of the toolkit is to accelerate the creation and development of MVVM applications in Xamarin.Android, Xamarin.iOS, Xamarin.Forms, Wi…

C++ //练习 17.17 更新你的程序,令它查找输入序列中所有违反“ei“语法规则的单词。

C Primer&#xff08;第5版&#xff09; 练习 17.17 练习 17.17 更新你的程序&#xff0c;令它查找输入序列中所有违反"ei"语法规则的单词。 环境&#xff1a;Linux Ubuntu&#xff08;云服务器&#xff09; 工具&#xff1a;vim 代码块&#xff1a; /**********…

定制开发AI智能名片O2O商城小程序:基于限量策略与个性化追求的营销创新

摘要:随着科技的飞速发展和消费者需求的日益多元化&#xff0c;传统商业模式正经历着前所未有的变革。在数字化转型的大潮中&#xff0c;定制开发AI智能名片O2O商城小程序作为一种新兴的商业模式&#xff0c;凭借其独特的个性化定制能力、高效的线上线下融合&#xff08;O2O&am…

居住证申报系统小程序的设计

管理员账户功能包括&#xff1a;系统首页&#xff0c;个人中心&#xff0c;群众用户管理&#xff0c;警方管理&#xff0c;居住证登记管理&#xff0c;回执单管理&#xff0c;领证信息管理&#xff0c;公告栏管理&#xff0c;系统管理 微信端账号功能包括&#xff1a;系统首页…

MySQL数据库专栏(四)数据库操作

1、创建数据库 create database if not exists [数据库名称] character set [字符集] COLLATE [排序规则]; 例如&#xff1a;create database if not exists db_demo character set utf8mb4 COLLATE utf8mb4_general_ci; if not exists&#xff1a;判断数据库是否存在&#x…

Ubuntu20, Windows10 双系统安装

1. 制作启动盘 1.1 下载 Ubuntu 系统镜像 ISO 文件 从 Ubuntu 官网下载 (https://cn.ubuntu.com/download/desktop)。官网访问慢的&#xff0c;从国内镜像点下。 1.2 烧录 Ubuntu ISO 镜像 下载 Rufus&#xff1a;从Rufus官网下载 Rufus 工具。 插入U 盘&#xff1a;将U盘插…

详解语义安全(semantically secure)

目录 一. 引入 二. 密文与明文 2.1 通俗性理解 2.2 定理 2.3 定理理解 三. 语义安全的第一个版本 3.1 基本理解 3.2 定理 3.3 定理理解 四. 语义安全的第二个版本 4.1 直观解释 4.2 小结 一. 引入 密码学中安全加密要求&#xff1a;敌手&#xff08;adversary&…

“百板齐发“ — 一个拥有上百个独特看板的代码

精选100套AXURE可视化数据大屏看板 产品经理高效工具 高保真原型模板 赋能智慧城市 元件库 AXURE9.0版本&#xff0c;所有页面均可编辑&#xff0c;部分地图与实现放大缩小&#xff0c;拖拉拽的功能。 01.水质情况实时监测预警系统 02.全国停车云实时数据监测系统 03.中国移…

【系统分析师】-综合知识-补充知识

1、在软件设计中&#xff0c;通常由着眼于“宏观的”软件架构开始&#xff0c;由着眼于“微观的”构件模块结束。 即测试应该从“微观”开始&#xff0c;逐步转向“宏观”。 2、数据备份是信息系统运行管理时保护数据的重要措施。增量备份可针对上次任何一种备份进行&#xf…

海康VisionMaster使用学习笔记7-Group模块

Group模块的使用 1. 添加图像源&#xff0c;导入图片 2. 添加快速匹配 对匹配框区域进行顶点检测 并循环10次(匹配框个数 从而检测出所有顶点)。 3. 添加Group工具 拖一个Group模块 点击设置 输入设置 添加图像源输入数据 添加快速匹配的匹配框 4. 双击Group工具块进入 添加…