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