Qt + FFmpeg在Linux下音视频播放器

参考文章

说明

     Linux的系统为:中标麒麟

     Qt版本:5.9.6

     FFmpeg版本:4.0


(备注:Qt与FFmpeg均为跨平台,所以本工程是可在其他系统运行)

     

效果

     模块简介:

          涉及视音频读码、队列缓冲、解码、同步、播放等。

FFmpeg+Qt在Linux系统下编写的视频播放器。

流程

在这里插入图片描述

     PTS和DTS

          音视频流中的每一帧都有时间相关的信息,其中PTS是播放时间,DTS是解码时间。音频的PTS和DTS是一致的,而某些视频各种中可能会存在DTS和PTS不一致的帧,我们这里主要通过PTS来控制播放时间。对解码后的AVFrame使用av_frame_get_best_effort_timestamp可以获取PTS。
time_base
          我们注意到PTS是一个整形数据,time_base是PTS的单位,PTS乘以time_base即可得到实际时间。只有AVStream中获取的time_base才是对的,其他地方获取的可能会有问题。

     音视频同步策略

          一般来说有三种方式,音频同步到视频,视频同步到音频,音视频同步到外部时间。一般各个参数设置正确音频就能够以正常的速度播放,所以把视频同步到音频在一般情况下,是一个简单有效的同步策略。本文主要采取这个方式同步音视频,来展示相关的基本思路。



     1、解协议、解封装、配置视音频

#define MSG(str)        QMessageBox::warning(0,"error",str,QMessageBox::Ok)

/// ffmpeg共享
namespace FFMPEG_COMMON
{
    
    
    /// 由错误码查找错误原因
    static QString FindErrorString(const int &index)
    {
    
    
        char buf[256] = {
    
    0};
        av_strerror(index,buf,256);
        return QString(buf);
    }
}

/// 打开媒体前,记得先进行关闭媒体操作
void FFMPEG::CloseMedia()
{
    
    
    /// 视音频索引
    VideoIndex = AudioIndex = -1;

    /// 解码器上下文
    avcodec_close(VCodecContext);
    avcodec_free_context(&VCodecContext);
    avcodec_close(ACodecContext);
    avcodec_free_context(&ACodecContext);

    /// 格式上下文
    avformat_close_input(&avFormatContext);
}

/// 打开媒体 - 解协议、解封装、配置视音频
bool FFMPEG::OpenMedia(const char *url)
{
    
    
    /// 打开媒体状态
    bool OpenMediaState = false;

    /// 检测媒体路径是否为空
    short urlLen = strlen(url);
    if(urlLen > 0)
    {
    
    
        /// 先关闭媒体文件,以保证打开顺利
        CloseMedia();

        /// 执行结果
        int result = -1;

        /// 打开输入上下文
        result = avformat_open_input(&avFormatContext,url,nullptr,nullptr);
        if(result != 0)
        {
    
    
            MSG("avformat_open_input - " + FFMPEG_COMMON::FindErrorString(result));
            return OpenMediaState;
        }

        /// 打开媒体信息
        result = avformat_find_stream_info(avFormatContext,nullptr);
        if(result < 0)
        {
    
    
            MSG("avformat_find_stream_info - " + FFMPEG_COMMON::FindErrorString(result));
            return OpenMediaState;
        }

         视频媒体处理
        /// 查找媒体码流索引
        VideoIndex = av_find_best_stream(avFormatContext,AVMediaType::AVMEDIA_TYPE_VIDEO,-1,-1,nullptr,0);

        /// 查找解码器
        AVCodec *VCodec = avcodec_find_decoder(avFormatContext->streams[VideoIndex]->codecpar->codec_id);
        if(!VCodec)
        {
    
    
            MSG("video - \"avcodec_find_decoder\" error!");
            return OpenMediaState;
        }

        /// 创建媒体解码上下文
        VCodecContext = avcodec_alloc_context3(VCodec);
        if(!VCodecContext)
        {
    
    
            MSG("video - \"avcodec_alloc_context3\" error!");
            return OpenMediaState;
        }

        /// 配置媒体解码器上下文
        result = avcodec_parameters_to_context(VCodecContext,avFormatContext->streams[VideoIndex]->codecpar);
        if(result < 0)
        {
    
    
            MSG("video - avcodec_parameters_to_context - " + FFMPEG_COMMON::FindErrorString(result));
            return OpenMediaState;
        }

        /// 打开媒体解码器上下文
        result = avcodec_open2(VCodecContext,VCodec,nullptr);
        if(result != 0)
        {
    
    
            MSG("video - avcodec_open2 - " + FFMPEG_COMMON::FindErrorString(result));
            return OpenMediaState;
        }

        /// 解码器指针置空
        VCodec = nullptr;
        

         音频媒体处理
        /// 查找媒体码流索引
        AudioIndex = av_find_best_stream(avFormatContext,AVMediaType::AVMEDIA_TYPE_AUDIO,-1,-1,nullptr,0);

        /// 查找解码器
        AVCodec *ACodec = avcodec_find_decoder(avFormatContext->streams[AudioIndex]->codecpar->codec_id);
        if(!ACodec)
        {
    
    
            MSG("audio - \"avcodec_find_decoder\" error!");
            return OpenMediaState;
        }

        /// 创建媒体解码上下文
        ACodecContext = avcodec_alloc_context3(ACodec);
        if(!ACodecContext)
        {
    
    
            MSG("audio - \"avcodec_alloc_context3\" error!");
            return OpenMediaState;
        }
        ACodecContext->thread_count = 4;

        /// 配置媒体解码器上下文
        result = avcodec_parameters_to_context(ACodecContext,avFormatContext->streams[AudioIndex]->codecpar);
        if(result < 0)
        {
    
    
            MSG("audio - avcodec_parameters_to_context - " + FFMPEG_COMMON::FindErrorString(result));
            return OpenMediaState;
        }

        /// 打开媒体解码器上下文
        result = avcodec_open2(ACodecContext,ACodec,nullptr);
        if(result != 0)
        {
    
    
            MSG("audio - avcodec_open2 - " + FFMPEG_COMMON::FindErrorString(result));
            return OpenMediaState;
        }

        /// 解码器指针置空
        ACodec = nullptr;
        

        /// 打开媒体状态完毕
        OpenMediaState = true;
    }

    /// 文件指针设置空
    url = nullptr;

    /// 返回打开媒体状态
    return OpenMediaState;
}

     2、循环读取,分配队列


/// 下列代码为线程内运行,将读取到的数据判断是否为音频或视频后,分别扔入对应的音视频线程
	while(ThreadFlag)
    {
    
    
        /// 读帧
        if(av_read_frame(avFormatContext,avPacket) == 0)
        {
    
    
            /// 判断帧类型
            if(avPacket->stream_index == VideoIndex)                /// 视频
            {
    
    
                / 拷贝新的AVPacket
                AVPacket *VPacket = av_packet_clone(avPacket);
                if(VPacket)
                {
    
    
                    /// 视频包加入缓冲队列。若缓冲队列满了则释放传入的视频包
                    video->Push(VPacket);
                }
            }
            else if(avPacket->stream_index == AudioIndex)           /// 音频
            {
    
    
                /// 拷贝新的AVPacket
                AVPacket *APacket = av_packet_clone(avPacket);
                if(APacket)
                {
    
    
                    /// 视频包加入缓冲队列。若缓冲队列满了则释放传入的音频包
                    audio->Push(APacket);
                }
            }

            /// 引用计数
            av_packet_unref(avPacket);
        }
        else
        {
    
    
            /// 判断音频、视频的另外两个线程是否结束。若结束则退出本线程循环
            if(video->isFinished() && audio->isFinished())
                break;
        }

        QThread::msleep(10);
    }

     3、视音频解码单例

/// 进行解码
bool DeCodec::Send(AVCodecContext *avCodecContext,AVPacket *avPacket)
{
    
    
    return avcodec_send_packet(avCodecContext,avPacket) == 0 ? true:false;
}

/// 解码后读帧 - 一次解码可能对应多次解码后读帧(一般音频会有)
bool DeCodec::Receive(AVCodecContext *avCodecContext,AVFrame *avFrame)
{
    
    
    return avcodec_receive_frame(avCodecContext,avFrame) == 0 ? true:false;
}

     4、音频播放

H

#ifndef AUDIOPLAY_H
#define AUDIOPLAY_H

#include <QAudioOutput>
#include <QIODevice>

class AudioPlay
{
    
    
public:
    AudioPlay(){
    
    }
    ~AudioPlay();

    /// 设置参数
    void SetParameter(const int &,const int &,const int &);

    /// 返回缓冲内还没有播放的时间(ms)
    long long GetPts();

    /// 打开音频播放
    virtual bool Open();
    virtual void Close();

    //播放音频
    bool Write(const unsigned char *data, int datasize);
    int GetFree();

private:
    QAudioOutput *AudioOutput = nullptr;
    QIODevice *AIODevice = nullptr;

    int sampleRate = 44100;
    int sampleSize = 16;
    int channels = 2;
};

#endif // AUDIOPLAY_H

CPP

#include "AudioPlay.h"

AudioPlay::~AudioPlay()
{
    
    
    Close();
}

void AudioPlay::SetParameter(const int &sr, const int &ss, const int &c)
{
    
    
    sampleRate = sr;
    sampleSize = ss;
    channels = c;
}

long long AudioPlay::GetPts()
{
    
    
    long long pts = 0;

    /// 还没播放的音频数
    double size = AudioOutput->bufferSize() - AudioOutput->bytesFree();

    /// 1秒音频字节数大小
    double secSize = sampleRate * (sampleSize / 8) *channels;

    if(secSize > 0)
    {
    
    
        pts = (secSize / size) * 1000;
    }

    return pts;
}

bool AudioPlay::Open()
{
    
    
    Close();

    QAudioFormat AudioFormat;
    AudioFormat.setSampleRate(sampleRate);
    AudioFormat.setSampleSize(sampleSize);
    AudioFormat.setChannelCount(channels);
    AudioFormat.setCodec("audio/pcm");
    AudioFormat.setByteOrder(QAudioFormat::LittleEndian);
    AudioFormat.setSampleType(QAudioFormat::UnSignedInt);

    AudioOutput = new QAudioOutput(AudioFormat);
    AIODevice = AudioOutput->start(); //开始播放
    if(AIODevice)
        return true;
    return false;
}

void AudioPlay::Close()
{
    
    
    if (AIODevice)
    {
    
    
        if(AIODevice->isOpen())
            AIODevice->close ();
    }
    AIODevice = nullptr;

    if (AudioOutput)
    {
    
    
        AudioOutput->stop();
        delete AudioOutput;
    }
    AudioOutput = nullptr;
}

bool AudioPlay::Write(const unsigned char *data, int datasize)
{
    
    
    if (!data || datasize <= 0)
        return false;

    if (!AudioOutput || !AIODevice)
    {
    
    
        return false;
    }
    int size = AIODevice->write((char *)data, datasize);

    if (datasize != size)
        return false;
    return true;
}

int AudioPlay::GetFree()
{
    
    
    if (!AudioOutput)
    {
    
    
        return 0;
    }
    int free = AudioOutput->bytesFree();
    return free;
}

     5、音频线程处理 ,并使用QAudioOutput进行配置播放

	/*下列代码均在音频线程内进行*/

	/// 音频重采样上下文初始化
    SwrContext *swrContext = swr_alloc();
    swrContext = swr_alloc_set_opts(
                                        swrContext,
                                        av_get_default_channel_layout(2),                           // 输出格式
                                        AV_SAMPLE_FMT_S16,                                          // 输出样本格式
                                        ACodecContext->sample_rate,                                 // 输出采样率
                                        av_get_default_channel_layout(ACodecContext->channels),     // 输入格式
                                        ACodecContext->sample_fmt,                                  // 输入样本格式
                                        ACodecContext->sample_rate,                                 // 输入采样率
                                        0,0
                                    );
    int result = swr_init(swrContext);
    if(result !=0 )
    {
    
    
        qDebug() << "音频初始化失败!" << endl;
        ThreadFlag = false;
    }

    unsigned char *pcm = nullptr;

    /// 当空包累加次数为10则表明包读完
    unsigned short PacketEmptyCnt = 0;

    while(ThreadFlag)
    {
    
    
        if(!APackets.isEmpty())
        {
    
    
            PacketEmptyCnt = 0;

            /// 取出一包进行处理
            Mutex.lock();
            AVPacket *APacket = APackets.front();
            APackets.pop_front();
            Mutex.unlock();

            if(APacket)
            {
    
    
                /// 进行解码
                bool SendFlag = DECODEC->Send(ACodecContext,APacket);

                /// 解码成功
                if(SendFlag)
                {
    
    
                    /// 从解码处取出,一次解码对应多次接收成功
                    while(ThreadFlag)
                    {
    
    
                        /// 创建临时接收解码帧
                        AVFrame *AFrame = av_frame_alloc();

                        /// 接收解码帧
                        bool ReceiveFlag = DECODEC->Receive(ACodecContext,AFrame);

                        /// 接收解码帧成功
                        if(ReceiveFlag)
                        {
    
    
                            /// 获取音频pts
                            DECODEC->AudioPts = AFrame->pkt_pts * av_q2d(TimeBase);

                            uint8_t *data[2] = {
    
    0};
                            if(!pcm)
                            {
    
    
                                pcm = new uint8_t[AFrame->nb_samples*2*2];
                            }
                            data[0] = pcm;

                            int result = swr_convert
                                         (
                                             swrContext,
                                             data,AFrame->nb_samples,                                    /// 输出
                                             (const uint8_t **)(AFrame->data),AFrame->nb_samples         /// 输入
                                         );

                            int size = result * AFrame->channels * av_get_bytes_per_sample((AVSampleFormat)AV_SAMPLE_FMT_S16);

                            while(ThreadFlag)
                            {
    
    
                                if (size <= 0)
                                    break;

                                /// 缓冲未播完,空间不够
                                if (audioPlay->GetFree() < size)
                                {
    
    
                                    //msleep(1);
                                    continue;
                                }

                                audioPlay->Write(pcm, size);
                                break;
                            }
                        }

                        /// 释放这一帧
                        av_frame_free(&AFrame);

                        /// 若接收解码帧失败则退出循环接收
                        if(!ReceiveFlag)
                        {
    
    
                            break;
                        }
                    }
                }

                /// 释放这一包
                av_packet_free(&APacket);
            }
        }
        else
        {
    
    
            if(++PacketEmptyCnt >= 10000)
            {
    
    
                break;
            }
        }
        QThread::usleep(1);
    }

     6、视频线程处理 ,并转为QImage播放

			/// 取出一包进行处理
            Mutex.lock();
            AVPacket *VPacket = VPackets.front();
            VPackets.pop_front();
            Mutex.unlock();

            if(VPacket)
            {
    
    
                /// 进行解码
                bool SendFlag = DECODEC->Send(VCodecContext,VPacket);

                /// 解码成功
                if(SendFlag)
                {
    
    
                    /// 从解码处取出,一次解码对应多次接收成功
                    while(ThreadFlag)
                    {
    
    
                        /// 创建临时接收解码帧
                        AVFrame *VFrame = av_frame_alloc();

                        /// 接收解码帧
                        bool ReceiveFlag = DECODEC->Receive(VCodecContext,VFrame);

                        /// 接收解码帧成功
                        if(ReceiveFlag)
                        {
    
    
                            /// 计算视频帧的pts - 为下面同步做基础
                            if(VPacket->pts == AV_NOPTS_VALUE)
                                DECODEC->VideoPts = 0.0;
                            else
                                DECODEC->VideoPts = av_frame_get_best_effort_timestamp(VFrame) * av_q2d(TimeBase);
                            ///

                            swsContext = sws_getCachedContext
                                         (
                                            swsContext,                             /// 传nullptr会新创建
                                            VFrame->width,                          /// 输入宽
                                            VFrame->height,                         /// 输入高
                                            (AVPixelFormat)(VFrame->format),        /// 输入格式
                                            VFrame->width,                          /// 输出宽
                                            VFrame->height,                         /// 输出高
                                            AV_PIX_FMT_BGRA,                        /// 输出格式
                                            SWS_FAST_BILINEAR,                      /// 转换算法
                                            nullptr,
                                            nullptr,
                                            nullptr
                                         );

                            if(swsContext)
                            {
    
    
                                if(rgb == nullptr)
                                {
    
    
                                    rgb = new unsigned char[VFrame->width*VFrame->height*4];
                                }

                                uint8_t *data[2]={
    
    0};
                                data[0] = rgb;

                                int lines[2] = {
    
    0};
                                lines[0] = VFrame->width * 4;

                                sws_scale
                                (
                                    swsContext,
                                    VFrame->data,
                                    VFrame->linesize,
                                    0,
                                    VCodecContext->height,
                                    avFrame_Dst->data,
                                    avFrame_Dst->linesize
                                );

                                QImage Image((uchar *)dBuffer,VCodecContext->width,VCodecContext->height,QImage::Format_RGB32);
                                if(!Image.isNull())
                                {
    
    
                                    VideoImage(Image);
                                }
                            }
                        }

     最后 - 视音频同步

                        /// - 音视频同步
                        double frameRate = av_q2d(AgvTimeBase);
                        frameRate += VFrame->repeat_pict * (frameRate * 0.5);
                        if (fabs(DECODEC->VideoPts - DECODEC->AudioPts) > 0.04 &&
                            fabs(DECODEC->VideoPts - DECODEC->AudioPts) < 10.0)
                        {
    
    
                            /// 如果视频比音频快,延迟差值播放,否则直接播放,这里没有做丢帧处理
                            if (DECODEC->VideoPts > DECODEC->AudioPts)
                            {
    
    
                                /// 除4是为了画面流畅
                                QThread::usleep((unsigned long)((DECODEC->VideoPts - DECODEC->AudioPts)*1000000/4));
                            }
                        }
                        ///

源码

正在上传…

关注

微信公众号搜索"Qt_io_"或"Qt开发者中心"了解更多关于Qt、C++开发知识.。

笔者 - jxd

猜你喜欢

转载自blog.csdn.net/automoblie0/article/details/108558773