• 【FFmpeg视频播放器开发】解封装类和解码类的封装(三)


    一、前言

    在上一篇中我们实现了视频和音频的解封装、解码及写文件,但其基本是堆出来的代码,可复用性以及扩展性比较低,现在我们对它进行类的封装。这里我们先只实现解封装类和解码类。

    二、XDemux类的实现(解封装)

    新创建个工程 XPlayer_2。然后我们看下 XDemux 类要实现哪些函数:

    #ifndef XDEMUX_H
    #define XDEMUX_H
    
    #include <iostream>
    #include <mutex>
    
    // 调用FFmpeg的头文件
    extern "C" {
    #include <libavcodec/avcodec.h>
    #include <libavformat/avformat.h>
    #include <libswscale/swscale.h>
    #include <libavutil/imgutils.h>
    }
    
    using namespace std;
    
    // 解封装类
    class XDemux
    {
    public:
    	XDemux();
    	virtual ~XDemux();
    
    	bool open(const char* url); // 打开媒体文件或者流媒体(rtsp、rtmp、http)
    	AVPacket* read(); // 读取一帧AVPacket
    	AVCodecParameters* copyVPara(); // 获取视频参数
    	AVCodecParameters* copyAPara(); // 获取音频参数
    	virtual bool isAudio(AVPacket* pkt); // 是否为音频
    	virtual bool seek(double pos); // seek位置(pos 0.0~1.0)
    	virtual void close(); // 关闭
    
    	int m_totalMs = 0; // 媒体总时长(毫秒)
    
    private:
    	std::mutex m_mutex; // 互斥锁
    	bool m_isFirst = true; // 是否第一次初始化,避免重复初始化
    
    	AVFormatContext* pFormatCtx = NULL; // 解封装上下文
    	int nVStreamIndex = -1; // 视频流索引
    	int nAStreamIndex = -1; // 音频流索引
    };
    
    #endif // XDEMUX_H
    

    2.1 构造函数

    XDemux::XDemux()
    {
        std::unique_lock<std::mutex> guard(m_mutex); // 加上锁,避免多线程同时初始化导致错误
        if(m_isFirst) {
            // 初始化网络库 (可以打开rtsp rtmp http 协议的流媒体视频)
            avformat_network_init();
            m_isFirst = false;
        }
    }
    

    进行 FFmpeg 的初始化。


    2.2 open():打开媒体文件或者流媒体

    // 打开媒体文件或者流媒体(rtsp、rtmp、http)
    bool XDemux::open(const char *url)
    {
    
        // 参数设置
        AVDictionary *opts = NULL;
        av_dict_set(&opts, "rtsp_transport", "tcp", 0); // 设置rtsp流以tcp协议打开
        av_dict_set(&opts, "max_delay", "500", 0); // 设置网络延时时间
    
        // 1、打开媒体文件
        std::unique_lock<std::mutex> guard(m_mutex);
        int nRet = avformat_open_input(
            &pFormatCtx,
            url,
            nullptr,  // nullptr表示自动选择解封器
            &opts // 参数设置
        );
        if (nRet != 0)
        {
            char errBuf[1024] = { 0 };
            av_strerror(nRet, errBuf, sizeof(errBuf));
            cout << "open " << url << " failed! :" << errBuf << endl;
            return false;
        }
        cout << "open " << url << " success! " << endl;
    
        // 2、探测获取流信息
        nRet = avformat_find_stream_info(pFormatCtx, 0);
        if (nRet < 0) {
            char errBuf[1024] = { 0 };
            av_strerror(nRet, errBuf, sizeof(errBuf));
            cout << "open " << url << " failed! :" << errBuf << endl;
            return false;
        }
    
        // 获取媒体总时长,单位为毫秒
        m_totalMs = static_cast<int>(pFormatCtx->duration / (AV_TIME_BASE / 1000));
        cout << "totalMs = " << m_totalMs << endl;
        // 打印视频流详细信息
        av_dump_format(pFormatCtx, 0, url, 0);
    
        // 3、获取视频流索引
        nVStreamIndex = av_find_best_stream(pFormatCtx, AVMEDIA_TYPE_VIDEO, -1, -1, NULL, 0);
        if (nVStreamIndex == -1) {
            cout << "find videoStream failed!" << endl;
            return false;
        }
        // 打印视频信息(这个pStream只是指向pFormatCtx的成员,未申请内存,为栈指针无需释放,下面同理)
        AVStream *pVStream = pFormatCtx->streams[nVStreamIndex];
        cout << "=======================================================" << endl;
        cout << "VideoInfo: " << nVStreamIndex << endl;
        cout << "codec_id = " << pVStream->codecpar->codec_id << endl;
        cout << "format = " << pVStream->codecpar->format << endl;
        cout << "width=" << pVStream->codecpar->width << endl;
        cout << "height=" << pVStream->codecpar->height << endl;
        // 帧率 fps 分数转换
        cout << "video fps = " << r2d(pVStream->avg_frame_rate) << endl;
    
        // 4、获取音频流索引
        nAStreamIndex = av_find_best_stream(pFormatCtx, AVMEDIA_TYPE_AUDIO, -1, -1, NULL, 0);
        if (nVStreamIndex == -1) {
            cout << "find audioStream failed!" << endl;
            return false;
        }
        // 打印音频信息
        AVStream *pAStream = pFormatCtx->streams[nAStreamIndex];
        cout << "=======================================================" << endl;
        cout << "AudioInfo: " << nAStreamIndex  << endl;
        cout << "codec_id = " << pAStream->codecpar->codec_id << endl;
        cout << "format = " << pAStream->codecpar->format << endl;
        cout << "sample_rate = " << pAStream->codecpar->sample_rate << endl;
        // AVSampleFormat;
        cout << "channels = " << pAStream->codecpar->channels << endl;
        // 一帧数据?? 单通道样本数
        cout << "frame_size = " << pAStream->codecpar->frame_size << endl;
    
        return true;
    }
    

    这个 open() 函数实现了视频的解封装,重点是获得了解封装上下文,以及视频流索引和音频流索引。注意事项:

    • 由于后面要使用多线程来解码播放,提高效率并避免阻塞 GUI,所以上面加入了锁std::unique_lock来保护共享区域,后面同理。
    • 注意内存泄漏,上面的pFormatCtx申请了内存,后面使用之后要使用clear或者close()释放内存,这两个函数后面介绍。

    2.3 read():读取一帧AVPacket

    // 确保time_base的分母不为0
    static double r2d(AVRational r)
    {
        return r.den == 0 ? 0 : (double)r.num / (double)r.den;
    }
    
    // 读取一帧AVPacket(由于返回值指针申请了内存,函数内未释放,
    // 所以到调用时要记得释放,否则多次调用会造成内存泄漏,下面函数同理)
    AVPacket *XDemux::read()
    {
        std::unique_lock<std::mutex> guard(m_mutex);
    
        // 容错处理,确保即使视频未打开也不会崩溃
        if (!pFormatCtx)
        {
            return nullptr;
        }
    
        // 读取一帧,并分配空间
        AVPacket *pkt = av_packet_alloc();
        int nRet = av_read_frame(pFormatCtx, pkt);
        if (nRet != 0) // 读取错误,或者帧读取完了
        {
            av_packet_free(&pkt);
            return nullptr;
        }
        // pts转换为毫秒
        pkt->pts = static_cast<int>(pkt->pts*((r2d(pFormatCtx->streams[pkt->stream_index]->time_base) * 1000)));
        pkt->dts = static_cast<int>(pkt->dts*((r2d(pFormatCtx->streams[pkt->stream_index]->time_base) * 1000)));
        cout << pkt->pts << " "<<flush;
    
        return pkt;
    }
    

    这里是读取一帧 AVPacket,后面是放到循环里进行循环读取,其保存了视频和音频的压缩数据。注意事项:

    • 每次使用pFormatCtx前,都要做容错处理,确保即使视频未打开也不会崩。否则如果忘记执行open(),调用pFormatCtx成员会异常退出,这都是为了程序的健壮性。
    • pkt新申请了内存,后面使用时会有新的AVPacket *指针指向它,以获取音视频压缩数据,使用完之后要记得及时释放。
    • 后面新申请内存的指针都要这样处理,记得使用完之后释放内存。你也可以观察下,很多地方都做了这样的释放操作。这就是 C 语言实现的 FFmpeg 的麻烦之处,时不时就容易出现内存泄漏,需要写代码时非常小心。

    2.4 copyVPara():获取音视频参数

    // 获取视频参数
    // 为什么不直接返回AVCodecParameters,而是间接拷贝,是为了避免多线程时一个线程调用open后close,
    // 另一个线程再去调用open()中的AVCodecParameters容易出错,获取音频参数同理
    AVCodecParameters *XDemux::copyVPara()
    {
        std::unique_lock<std::mutex> guard(m_mutex);
        if (!pFormatCtx)
            return nullptr;
        // 拷贝视频参数
        AVCodecParameters *pCodecPara = avcodec_parameters_alloc();
        avcodec_parameters_copy(pCodecPara, pFormatCtx->streams[nVStreamIndex]->codecpar);
    
        return pCodecPara;
    }
    
    // 获取音频参数
    AVCodecParameters *XDemux::copyAPara()
    {
        std::unique_lock<std::mutex> guard(m_mutex);
        if (!pFormatCtx)
            return nullptr;
        // 拷贝音频参数
        AVCodecParameters *pCodecPara = avcodec_parameters_alloc();
        avcodec_parameters_copy(pCodecPara, pFormatCtx->streams[nAStreamIndex]->codecpar);
    
        return pCodecPara;
    }
    

    前面查找到视频流和音频流索引了,就当然要根据索引获取视频参数和音频参数。

    • 可以看到这里也做了容错处理,确保即使视频未打开也不会崩。
    • 新申请内存的pCodecPara使用完之后,也要记得及时释放。

    2.5 isAudio():是否为音频

    // 是否为音频
    bool XDemux::isAudio(AVPacket *pkt)
    {
        if (!pkt) return false;
        if (pkt->stream_index == nVStreamIndex)
            return false;
    
        return true;
    }
    

    用来在后续的循环解码过程中,if 判断read()读取AVPacket的是视频流,还是音频流,来选择进行不同的解码操作。

    当然你也可以在循环解码时选择if (pkt->stream_index == XDemux::Get().getVStreamIndex())直接判断,封装本来就看个人的选择。


    2.6 seek():seek位置

    // seek位置(pos 0.0~1.0)
    bool XDemux::seek(double pos)
    {
        std::unique_lock<std::mutex> guard(m_mutex);
        if (!pFormatCtx)
            return false;
        // 清理先前未滑动时解码到的视频帧
        avformat_flush(pFormatCtx);
    
        long long seekPos  = static_cast<long long>(pFormatCtx->streams[nVStreamIndex]->duration * pos); // 计算要移动到的位置
        int nRet = av_seek_frame(pFormatCtx, nVStreamIndex, seekPos, AVSEEK_FLAG_BACKWARD | AVSEEK_FLAG_FRAME);
        if (nRet < 0)
            return false;
    
        return true;
    }
    

    这个函数是为了以后拖动进度做准备。注意事项:

    • 在我们点击滑动条更新视频位置后,由于此时缓冲区中还有先前未滑动时解码到的视频帧,这样的帧对于我们已经滑动后的位置已没有意义了,应该从缓冲区中清理掉。

    2.7 close():关闭

    // 关闭
    void XDemux::close()
    {
        std::unique_lock<std::mutex> guard(m_mutex);
        if (!pFormatCtx)
            return;
        // 释放解封装上下文申请空间
        avformat_flush(pFormatCtx);
        // 关闭解封装上下文
        avformat_close_input(&pFormatCtx);
        // 重新初始化媒体总时长(毫秒)
        m_totalMs = 0;
    }
    

    close()释放申请空间,同时重新初始化媒体总时长变量。

    三、XDecode类的实现(解码)

    我们先看下类的声明:

    // 解码类(视频和音频)
    class XDecode
    {
    public:
        XDecode();
        virtual ~XDecode();
    
        bool Open(AVCodecParameters *codecPara); // 打开解码器
        bool Send(AVPacket *pkt); // 发送到解码线程
        AVFrame* Recv(); // 获取解码数据
        void Close(); // 关闭
    
        bool m_isAudio = false; // 是否为音频的标志位
    
    private:
        AVCodecContext * m_VCodecCtx = 0; // 解码器
        std::mutex m_mutex; // 互斥锁
    };
    

    3.1 Open():打开解码器

    // 打开解码器
    bool XDecode::Open(AVCodecParameters *codecPara)
    {
        if (!codecPara) return false;
        Close();
    
        // 根据传入的para->codec_id找到解码器
        AVCodec *vcodec = avcodec_find_decoder(codecPara->codec_id);
        if (!vcodec)
        {
            avcodec_parameters_free(&codecPara);
            cout << "can't find the codec id " << codecPara->codec_id << endl;
            return false;
        }
        cout << "find the AVCodec " << codecPara->codec_id << endl;
    
        std::unique_lock<std::mutex> guard(m_mutex);
        // 创建解码器上下文
        m_VCodecCtx = avcodec_alloc_context3(vcodec);
        // 配置解码器上下文参数
        avcodec_parameters_to_context(m_VCodecCtx, codecPara);
        // 清空编码器参数,避免内存泄漏(很重要)
        avcodec_parameters_free(&codecPara);
        // 八线程解码
        m_VCodecCtx->thread_count = 8;
    
        // 打开解码器上下文
        int nRet = avcodec_open2(m_VCodecCtx, 0, 0);
        if (nRet != 0)
        {
            avcodec_free_context(&m_VCodecCtx); // 失败这里就释放申请内存,否则留到不再使用后再释放
            char buf[1024] = { 0 };
            av_strerror(nRet, buf, sizeof(buf) - 1);
            cout << "avcodec_open2  failed! :" << buf << endl;
            return false;
        }
        cout << "avcodec_open2 success!" << endl;
    
        return true;
    }
    

    3.2 Send():发送解码AVPacket

    // 发送到解码线程(不管成功与否都释放pkt空间 对象和媒体内容)
    bool XDecode::Send(AVPacket *pkt)
    {
        // 容错处理
        if (!pkt || pkt->size <= 0 || !pkt->data) return false;
        std::unique_lock<std::mutex> guard(m_mutex);
        if (!m_VCodecCtx)
        {
            return false;
        }
        int nRet = avcodec_send_packet(m_VCodecCtx, pkt);
    
        // 无论成功与否,都清空AVPacket,避免内存泄漏(很重要)
        av_packet_free(&pkt);
        if (nRet != 0)
            return false;
    
        return true;
    }
    

    3.3 Recv():接受解码AVPacket

    // 获取解码数据,一次send可能需要多次Recv,获取缓冲中的数据Send NULL在Recv多次
    // 每次复制一份,由调用者释放 av_frame_free(如果是视频,接受的是YUV数据)
    AVFrame* XDecode::Recv()
    {
        std::unique_lock<std::mutex> guard(m_mutex);
        if (!m_VCodecCtx)
        {
            return NULL;
        }
        AVFrame *frame = av_frame_alloc();
        int nRet = avcodec_receive_frame(m_VCodecCtx, frame);
        if (nRet != 0)
        {
            av_frame_free(&frame); // 失败这里就释放申请内存,否则留到实际使用那里再释放
            return NULL;
        }
        cout << "["<<frame->linesize[0] << "] " << flush;
        return frame;
    }
    

    3.4 Close():关闭

    // 关闭
    void XDecode::Close()
    {
        std::unique_lock<std::mutex> guard(m_mutex);
        if (m_VCodecCtx)
        {
            avcodec_flush_buffers(m_VCodecCtx); // 清理解码器申请内存
            avcodec_close(m_VCodecCtx);
            avcodec_free_context(&m_VCodecCtx); // 关闭也要清理解码器申请内存
        }
    }
    

    四、客户端实现

    int main(int argc, char* argv[])
    {		
    	//=================1、解封装测试====================
    	const char* url = "dove_640x360.mp4";
    	XDemux demux; // 测试XDemux
    	cout << "demux.Open = " << demux.open(url);
    	demux.read();
    	cout << "CopyVPara = " << demux.copyVPara() << endl;
    	cout << "CopyAPara = " << demux.copyAPara() << endl;
    	cout << "seek=" << demux.seek(0.95) << endl;
    
    	//=================2、解码测试====================
    	XDecode decode; // 测试XDecode
    	cout << "vdecode.Open() = " << decode.Open(demux.copyVPara()) << endl;
    	XDecode adecode;
    	cout << "adecode.Open() = " << adecode.Open(demux.copyAPara()) << endl;
    
    	while(1)
    	{
    		AVPacket* pkt = demux.read();
    		if (demux.isAudio(pkt))
    		{
    			adecode.Send(pkt);
    			AVFrame* frame = adecode.Recv();
    			cout << "Audio:" << frame << endl;
    		}
    		else
    		{
    			decode.Send(pkt);
    			AVFrame* frame = decode.Recv();
    			cout << "Video:" << frame << endl;
    		}
    		if (!pkt) break;
    	}
    	
    	// 释放申请内存
    	demux.close();
    	decode.Close();
    
    	// 等待进程退出
    	system("pause");
    
    	return 0;
    }
    

    输出如下:

        Stream #0:0(und): Video: h264 (Main) (avc1 / 0x31637661), yuv420p, 640x360 [SAR 1:1 DAR 16:9], 418 kb/s, 24 fps, 24 tbr, 24k tbn, 48 tbc (default)
        Metadata:
          creation_time   : 2015-06-30T08:50:40.000000Z
          handler_name    : TrackHandler
        Stream #0:1(und): Audio: aac (LC) (mp4a / 0x6134706D), 48000 Hz, stereo, fltp, 49 kb/s (default)
        Metadata:
          creation_time   : 2015-06-30T08:50:40.000000Z
          handler_name    : Sound Media Handler
    =======================================================
    VideoInfo: 0
    codec_id = 28
    format = 0
    width=640
    height=360
    video fps = 24
    =======================================================
    AudioInfo: 1
    codec_id = 86018
    format = 8
    sample_rate = 48000
    channels = 2
    frame_size = 1024
    demux.Open = 10 CopyVPara = 053E2E20
    CopyAPara = 053E2EC0
    seek=1
    

    五、代码下载

    下载链接:https://github.com/confidentFeng/FFmpeg/tree/master/XPlayer/XPlayer_2


  • 相关阅读:
    关于1961年4月16日尤文图斯91国际米兰的故事
    《转》struts2动态方法配置 Action,使一个Action可处理多请求
    struts2跳转后总是会返回input
    CentOS设置服务开机自动启动【转】
    CentOS 6.2系统安装后的初始环境设置
    Ubuntu安装小技巧 拔掉网线
    虚拟机最小安装CentOS 6.2
    CentOS 6.2配置MySQL服务器
    CentOS修改机器名称
    配置GNOME环境
  • 原文地址:https://www.cnblogs.com/linuxAndMcu/p/14706012.html
Copyright © 2020-2023  润新知