【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

原文链接: https://www.cnblogs.com/linuxAndMcu/p/14706012.html

欢迎关注

微信关注下方公众号,第一时间获取干货硬货;公众号内回复【pdf】免费获取数百本计算机经典书籍

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

原创文章受到原创版权保护。转载请注明出处:https://www.ccppcoding.com/archives/209893

非原创文章文中已经注明原地址,如有侵权,联系删除

关注公众号【高性能架构探索】,第一时间获取最新文章

转载文章受原作者版权保护。转载请注明原作者出处!

(0)
上一篇 2023年2月13日 上午12:05
下一篇 2023年2月13日 上午12:06

相关推荐