记录 音、视频输出类封装

说明

模块化的设计思想十分重要,极大减少重复的无意义劳动,不在已经掌握的知识上反复浪费时间。本文记录下自己封装好的音频视频输出类,直接实例化类,先设置对应的音视频参数,之后调用接口传入解码后音、视频数据即可播放。封装类主要以 Qt + FFmpeg + SDL 进行设计。

视频输出类

视频输出即图像解码后的数据渲染。方式不唯一,举例现有一帧FFmpeg解码后的数据videoFrame,格式为yuv420P。

首先可以用Qt本身的功能实现:
1、CPU方式,将videoFrame的格式转为RGB32,构造QImage后发送给界面线程渲染
2、GPU方式,利用QOpenglWidget,直接渲染yuv420P的videoFrame
使用Qt方式明显优势是不需引入三方库,即能实现两种方式的渲染。劣势是代码量偏大尤其是CPU方式还要进行格式转换发送至界面线程,且代码逻辑存在差异不方便进行两种方式的灵活切换。

因此个人更喜欢使用成熟的久经考验的跨平台SDL库,虽然另外引入了SDL库,但劣势得以比较完美的解决。同样支持CPU、GPU两种方式渲染,只需要一个参数来切换模式,十分灵活。并且观察SDL的渲染性能应该略好于Qt方式。个人之前博客文章中有详细叙述 SDL渲染AVFrame

音频输出类

音频输出即播放解码后pcm数据。方式不唯一,相较于视频,音频的数据量小很多,处理方式也较为简单些。完全可以使用SDL来做,但是SDL的音频一般是通过内部回调方式,逻辑上会稍复杂。而Qt的音频输出接口属于同步逻辑,更为适合。

封装类代码

以下是封装的类代码,做好基本的环境配置,复制直接可用。举例视频输出:
1.setShowHandle 设置播放句柄
2.setVideoParm 设置播放参数
3.startDisplay 持续填充视频解码数据

1、头文件 mediaoutput.h

#ifndef MEDIAOUTPUT_H
#define MEDIAOUTPUT_H

#include <QAudioFormat>
#include <QAudioOutput>
#include <QDebug>
#include "ffmpegheader.h"
#include "SDL.h"

//视频输出类 SDL
class VideoOutput
{
public:
    VideoOutput();
    ~VideoOutput();

    //只显示不负责释放 AVFrame为YUV420P格式
    void startDisplay(AVFrame *videoFrame);
    void stopDisplay();

    //设置渲染窗口句柄
    void setShowHandle(void *id);
    //设置视频宽、高、渲染方式
    bool setVideoParm(int width, int height, bool cpuMode = true);
    //释放
    void release();

private:
    //初始化状态
    bool hasInit;

    //用到的相关变量
    SDL_Window *sdlWindow;
    SDL_Renderer *sdlRender;
    SDL_Texture *sdlTexture;

    //显示的句柄
    void *showId;
};


//音频输出类 Qt自带
class AudioOutput
{
public:
    AudioOutput();
    ~AudioOutput();

    //只播放不负责释放 AVFrame为解码后音频帧
    void startPlay(AVFrame *audioFrame);

    //设置音频参数 采样率 采样格式 通道数 均为ffmpeg解析到的参数
    bool setAudioParm(int sampleRate, int sampleFmt, int channels);
    //释放
    void release();

private:
    //初始化状态
    bool hasInit;

    //音频参数
    int sampleRate;
    int sampleSize;
    int channels;

    //设备输出
    QAudioOutput *audioOutput;
    QIODevice *audioDevice;

    //重采样
    SwrContext *audioSwrCtx;
    uint8_t *audioData;
};

#endif // MEDIAOUTPUT_H

2、实现文件 mediaoutput.cpp

#include "mediaoutput.h"

VideoOutput::VideoOutput()
{
    sdlWindow = nullptr;
    sdlRender = nullptr;
    sdlTexture = nullptr;
    showId = nullptr;

    hasInit = false;
}

VideoOutput::~VideoOutput()
{
    release();
}

void VideoOutput::startDisplay(AVFrame *videoFrame)
{
    if(!hasInit)
    {
        qDebug() << "视频参数未设置";
        return;
    }

    if(videoFrame == nullptr)
    {
        qDebug() << "视频帧无效";
        return;
    }

    //句柄尺寸变化 那么SDL渲染的窗口也会跟着变化
    SDL_Surface *sdlSurface = SDL_GetWindowSurface(sdlWindow);
    if (!sdlSurface)
    {
        qDebug() << "SDL_GetWindowSurface fail" << SDL_GetError();
        return;
    }

    //渲染
    int ret = SDL_UpdateYUVTexture(sdlTexture, NULL, videoFrame->data[0], videoFrame->linesize[0], videoFrame->data[1], videoFrame->linesize[1], videoFrame->data[2], videoFrame->linesize[2]);
    if (ret != 0)
        qDebug() << "SDL_UpdateYUVTexture fail" << SDL_GetError();

    ret = SDL_RenderClear(sdlRender);
    if (ret != 0)
        qDebug() << "SDL_RenderClear fail" << SDL_GetError();

    ret = SDL_RenderCopy(sdlRender, sdlTexture, NULL, NULL);
    if (ret != 0)
        qDebug() << "SDL_RenderCopy fail" << SDL_GetError();

    SDL_RenderPresent(sdlRender);
}

void VideoOutput::stopDisplay()
{
    if(!hasInit)
        return;

    SDL_SetRenderDrawColor(sdlRender, 240, 240, 240, 100);
    SDL_RenderClear(sdlRender);
    SDL_RenderPresent(sdlRender);
}

void VideoOutput::setShowHandle(void *id)
{
    this->showId = id;
}

bool VideoOutput::setVideoParm(int width, int height, bool cpuMode)
{
    if(this->showId == nullptr)
    {
        qDebug() << "video showId is nullptr";
        return false;
    }

    //SDL存在界面伸缩有时画面卡住 查阅资料应该是与SDL内部逻辑处理冲突 多次测试采用重置参数方法更为直接有效
    release();

    //默认cpu软件渲染模式 相比gpu硬件渲染兼容性更高
    SDL_RendererFlags flag;
    if(cpuMode)
        flag = SDL_RENDERER_SOFTWARE;
    else
        flag = SDL_RENDERER_ACCELERATED;

    sdlWindow = SDL_CreateWindowFrom(showId);
    if(!sdlWindow)
    {
        qDebug() << "SDL_CreateWindowFrom error" << SDL_GetError();
        return false;
    }

    sdlRender = SDL_CreateRenderer(sdlWindow, -1, flag);
    if (!sdlRender)
    {
        qDebug() << "SDL_CreateRenderer error" << SDL_GetError();
        return false;
    }

    sdlTexture = SDL_CreateTexture(sdlRender, SDL_PIXELFORMAT_IYUV, SDL_TEXTUREACCESS_STREAMING, width, height);
    if (!sdlTexture)
    {
        qDebug() << "SDL_CreateRenderer error" << SDL_GetError();
        return false;
    }

    SDL_ShowWindow(sdlWindow);
    hasInit = true;
    return true;
}

void VideoOutput::release()
{
    if(sdlTexture)
    {
        SDL_DestroyTexture(sdlTexture);
        sdlTexture = nullptr;
    }

    if(sdlRender)
    {
        SDL_DestroyRenderer(sdlRender);
        sdlRender = nullptr;
    }

    if(sdlWindow)
    {
        SDL_DestroyWindow(sdlWindow);
        sdlWindow = nullptr;
    }

    hasInit = false;
}


AudioOutput::AudioOutput()
{
    audioOutput = nullptr;
    audioDevice = nullptr;
    audioSwrCtx = nullptr;
    audioData = nullptr;
    sampleRate = 0;
    sampleSize = 0;
    channels = 0;
    hasInit = false;
}

AudioOutput::~AudioOutput()
{
    release();
}

void AudioOutput::startPlay(AVFrame *audioFrame)
{
    if(!hasInit)
    {
        qDebug() << "音频参数未设置";
        return;
    }

    if(audioFrame == nullptr)
    {
        qDebug() << "音频帧无效";
        return;
    }

    int outSize = av_samples_get_buffer_size(NULL, channels, audioFrame->nb_samples, AV_SAMPLE_FMT_S16, 1);

    int result = swr_convert(audioSwrCtx, &audioData, outSize, (const uint8_t **)audioFrame->data, audioFrame->nb_samples);
    if(result >= 0)
        audioDevice->write((char *)audioData, outSize);

}

bool AudioOutput::setAudioParm(int sampleRate, int sampleFmt, int channels)
{
    //释放上次重置即可
    release();

    this->sampleRate = sampleRate;
    this->sampleSize = av_get_bytes_per_sample((AVSampleFormat)sampleFmt) / channels;
    this->channels = channels;

    QAudioFormat format;
    format.setCodec("audio/pcm");
    format.setSampleRate(sampleRate);
    format.setSampleSize(sampleSize * 8);
    format.setChannelCount(channels);
    format.setSampleType(QAudioFormat::SignedInt);
    format.setByteOrder(QAudioFormat::LittleEndian);

    QAudioDeviceInfo info(QAudioDeviceInfo::defaultOutputDevice());

    bool res = info.isFormatSupported(format);
    if(!res)
    {
        qDebug() << "AudioFormat not Support";
        return false;
    }

    audioOutput = new QAudioOutput(QAudioDeviceInfo::defaultOutputDevice(), format);
    //设置缓存避免播放音频卡顿  太大可能会分配内存失败导致崩溃
    audioOutput->setBufferSize(40960);
    audioOutput->setVolume(1.0);
    audioDevice = audioOutput->start();

    //设置音频转换
    audioSwrCtx = swr_alloc();
    int64_t channelIn = av_get_default_channel_layout(channels);
    int64_t channelOut = channelIn;
    audioSwrCtx = swr_alloc_set_opts(audioSwrCtx, channelOut, AV_SAMPLE_FMT_S16, sampleRate, channelIn, (AVSampleFormat)sampleFmt, sampleRate, 0, 0);
    if(0 > swr_init(audioSwrCtx))
    {
        qDebug() << "audio swr_init error";
        release();
        return false;
    }

    //分配音频数据内存19200 可调节
    audioData = (uint8_t *)av_malloc(19200 * sizeof(uint8_t));

    hasInit = true;
    return true;
}

void AudioOutput::release()
{
    if(audioDevice)
        audioDevice->close();
    if(audioSwrCtx)
        swr_free(&audioSwrCtx);
    if(audioData)
        av_free(audioData);
    if(audioOutput)
        audioOutput->deleteLater();

    hasInit = false;
}
  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

你是周小哥啊

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值