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

        RTMP推流客户端是一种能够将音视频数据推送到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搭建指南进行操作。

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

 五、相关文章

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

Windosw下Visual Studio2022编译OpenCV-CSDN博客

  • 13
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
以下是一个使用QtOpenCV进行RTMP推流的例程: 首先,需要确保已经安装了OpenCVFFmpeg,并且Qt项目中已经添加了OpenCVFFmpeg的库文件。 然后,需要添加以下头文件: ```cpp #include <opencv2/opencv.hpp> #include <opencv2/highgui/highgui.hpp> #include <opencv2/imgproc/imgproc.hpp> #include <opencv2/videoio/videoio.hpp> #include <opencv2/videoio/legacy/constants_c.h> #include <QDebug> #include <QThread> #include <QString> #include <QMutex> #include <QDateTime> #include <QStringList> #include <QCoreApplication> ``` 接下来,需要声明一些变量: ```cpp // RTMP 推流地址 const char *rtmpUrl = "rtmp://localhost/live/stream"; // 视频编码器 const char *codecName = "libx264"; // 视频分辨率 const int width = 640; const int height = 480; // 视频帧率 const int fps = 25; // 视频比特率 const int bitrate = 1000000; // 帧计数器 int frameCount = 0; // RTMP 上下文 RTMP *rtmp = NULL; // 视频编码器上下文 AVCodecContext *codecCtx = NULL; // 视频帧 AVFrame *frame = NULL; // 视频帧缓存区 uint8_t *frameBuffer = NULL; // 视频帧缓存区大小 int frameBufferSize = 0; // 视频编码器 AVCodec *codec = NULL; // 编码器参数 AVDictionary *codecParams = NULL; // 编码器帧计数器 int64_t codecFrameCount = 0; // 编码器 PTS 计数器 int64_t codecPtsCount = 0; // 编码器上一帧 PTS int64_t codecLastPts = 0; // 编码器上一帧 DTS int64_t codecLastDts = 0; // 编码器视频流 AVStream *codecStream = NULL; // 编码器视频帧 AVFrame *codecFrame = NULL; // 编码器视频帧缓存区 uint8_t *codecFrameBuffer = NULL; // 编码器视频帧缓存区大小 int codecFrameBufferSize = 0; // 编码器视频帧计数器 int codecFrameCount = 0; // 编码器视频帧时间戳 int64_t codecFramePts = 0; // 编码器视频帧数据大小 int codecFrameDataSize = 0; // 编码器视频帧数据 uint8_t *codecFrameData = NULL; // 编码器视频帧数据指针 uint8_t *codecFrameDataPtr = NULL; // 编码器视频帧数据长度 int codecFrameDataLen = 0; // 编码器视频帧类型 int codecFrameType = 0; // 编码器视频帧是否为关键帧 int codecFrameKeyframe = 0; // 编码器视频帧时间基数 AVRational codecTimeBase = {1, 1000}; // 编码器视频帧时间戳增量 AVRational codecTimeIncrement = {1, fps}; // 编码器视频帧时间戳 AVRational codecPtsTimeBase = {1, AV_TIME_BASE}; ``` 接着,需要定义一个函数用于初始化编码器: ```cpp void initEncoder() { // 初始化编码器 avcodec_register_all(); // 获取编码器 codec = avcodec_find_encoder_by_name(codecName); // 创建编码器上下文 codecCtx = avcodec_alloc_context3(codec); codecCtx->width = width; codecCtx->height = height; codecCtx->bit_rate = bitrate; codecCtx->time_base = codecTimeBase; codecCtx->framerate = codecTimeIncrement; codecCtx->gop_size = fps; codecCtx->pix_fmt = AV_PIX_FMT_YUV420P; codecCtx->flags |= AV_CODEC_FLAG_GLOBAL_HEADER; // 设置编码器参数 av_dict_set(&codecParams, "preset", "ultrafast", 0); av_dict_set(&codecParams, "tune", "zerolatency", 0); // 打开编码器 avcodec_open2(codecCtx, codec, &codecParams); // 创建视频帧 codecFrame = av_frame_alloc(); codecFrame->format = codecCtx->pix_fmt; codecFrame->width = codecCtx->width; codecFrame->height = codecCtx->height; codecFrame->pts = 0; av_frame_get_buffer(codecFrame, 0); // 创建视频帧缓存区 codecFrameBufferSize = av_image_get_buffer_size(codecCtx->pix_fmt, codecCtx->width, codecCtx->height, 1); codecFrameBuffer = (uint8_t *)av_malloc(codecFrameBufferSize); av_image_fill_arrays(codecFrame->data, codecFrame->linesize, codecFrameBuffer, codecCtx->pix_fmt, codecCtx->width, codecCtx->height, 1); } ``` 然后,需要定义一个函数用于连接RTMP服务器: ```cpp bool connectRTMP() { // 初始化 RTMP RTMP_Init(rtmp); // 设置 RTMP URL if (!RTMP_SetupURL(rtmp, (char *)rtmpUrl)) { qDebug() << "RTMP_SetupURL error:" << rtmpUrl; return false; } // 设置连接超时时间 rtmp->Link.timeout = 5; // 开始连接 RTMP 服务器 if (!RTMP_Connect(rtmp, NULL)) { qDebug() << "RTMP_Connect error:" << rtmpUrl; return false; } // 开始连接 RTMP 流 if (!RTMP_ConnectStream(rtmp, 0)) { qDebug() << "RTMP_ConnectStream error:" << rtmpUrl; return false; } return true; } ``` 接下来,需要定义一个函数用于初始化视频帧: ```cpp void initFrame() { // 创建视频帧 frame = cvCreateMat(height, width, CV_8UC3); // 创建视频帧缓存区 frameBufferSize = av_image_get_buffer_size(AV_PIX_FMT_YUV420P, width, height, 1); frameBuffer = (uint8_t *)av_malloc(frameBufferSize); } ``` 然后,需要定义一个函数用于发送视频帧: ```cpp bool sendFrame() { // 将视频帧转换为 YUV 格式 cvCvtColor(frame, frame, CV_BGR2YUV_I420); // 将视频帧数据写入编码器缓存区 codecFrame->pts = codecPtsCount; codecFrameDataPtr = codecFrameData; codecFrameDataLen = avcodec_encode_video2(codecCtx, codecFrameDataPtr, codecFrameBufferSize, codecFrame); codecFrameDataPtr += codecFrameDataLen; // 如果编码器缓存区已满,则发送视频帧 if (codecFrameDataLen > 0) { // 计算视频帧时间戳 codecFramePts = av_rescale_q(codecFrameCount, codecTimeIncrement, codecTimeBase); // 发送视频帧 AVPacket pkt; av_init_packet(&pkt); pkt.data = codecFrameData; pkt.size = codecFrameDataLen; pkt.pts = codecFramePts; pkt.dts = codecFramePts; pkt.duration = av_rescale_q(1, codecTimeIncrement, codecTimeBase); pkt.stream_index = codecStream->index; av_interleaved_write_frame(rtmp->opaque, &pkt); av_packet_unref(&pkt); // 更新帧计数器 codecFrameCount++; codecFrameData = NULL; codecFrameDataLen = 0; return true; } return false; } ``` 最后,需要定义一个函数用于发送视频流: ```cpp void sendStream() { while (true) { // 读取视频帧 cv::Mat image; capture >> image; // 如果视频帧为空,则退出 if (image.empty()) { break; } // 将视频帧数据写入视频帧缓存区 memcpy(frameBuffer, image.data, frameBufferSize); // 将视频帧数据写入视频帧 cvSetData(frame, frameBuffer, width * 3); // 发送视频帧 if (sendFrame()) { qDebug() << "sendFrame:" << frameCount << ":" << codecFrameCount << ":" << codecPtsCount; } // 更新帧计数器 frameCount++; codecPtsCount += av_rescale_q(1, codecTimeIncrement, codecTimeBase); } } ``` 最后,在主函数中调用以上函数即可: ```cpp int main(int argc, char *argv[]) { // 初始化 RTMP rtmp = RTMP_Alloc(); if (!rtmp) { qDebug() << "RTMP_Alloc error"; return -1; } // 初始化编码器 initEncoder(); // 初始化视频帧 initFrame(); // 连接 RTMP 服务器 if (!connectRTMP()) { return -1; } // 发送视频流 sendStream(); // 关闭编码器 avcodec_close(codecCtx); // 关闭 RTMPRTMP_Close(rtmp); // 释放 RTMP RTMP_Free(rtmp); // 释放视频帧缓存区 av_free(frameBuffer); // 释放视频帧 cvReleaseMat(&frame); return 0; } ``` 以上就是一个使用QtOpenCV进行RTMP推流的例程。需要注意的是,由于涉及到视频编码和网络传输等技术,代码实现较为复杂,需要仔细理解和调试。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值