在上一篇中我們實現了視頻的解碼、格式轉換,但其基本是堆出來的代碼,可複用性以及擴展性比較低,現在我們對它進行類的封裝。這裏我們把它分爲四個小部分。
1、重構封裝FFMpeg類完成打開和關閉視頻接口
2、重構讀取視頻幀接口
3、重構解碼接口
4、重構ToRGB接口
一、重構封裝FFMpeg類完成打開和關閉視頻接口
我們使用VS的類嚮導在該項目下添加XFFMpeg類,將上一篇中編輯好的視頻打開和關閉的部分代碼移植過來,同時進行一些調整,需要注意的時考慮到後面的多線程應用,在打開關閉以及處理時我們都加入了互斥鎖,如何處理看下方的代碼,註釋也都比較清晰。
XFFMpeg.h文件
#pragma once
#include <iostream>
#include <QMutex>
extern "C"
{
//調用FFMpeg的頭文件
#include <libavformat/avformat.h>
#include <libswscale/swscale.h>
}
class XFFmpeg
{
public:
static XFFmpeg *Get()//單件模式
{
static XFFmpeg ff;
return &ff;
}
////////////////////////////////////////
///打開視頻文件,如果上次已經打開會先關閉
///@para path 視頻文件路徑
///@return bool 成功失敗,失敗錯誤信息通過 GetError獲取
bool Open(const char *path);//打開視頻文件
void Close();//關閉文件
std::string GetError();//獲取錯誤信息
virtual ~XFFmpeg();
int totalMs=0;//總時長
protected:
char errorbuff[1024];//打開時發生的錯誤信息
XFFmpeg();
QMutex mutex;//互斥變量,多線程時避免同時間的讀寫
AVFormatContext *ic = NULL;//解封裝上下文
};
XFFMpeg.cpp
#include "XFFmpeg.h"
//調用FFMpeg的lib庫
#pragma comment(lib,"avformat.lib")
#pragma comment(lib,"avutil.lib")
#pragma comment(lib,"avcodec.lib")
#pragma comment(lib,"swscale.lib")
XFFmpeg::XFFmpeg()
{
errorbuff[0] = '\0';//初始化
av_register_all();//註冊FFMpeg的庫
}
XFFmpeg::~XFFmpeg()
{
}
bool XFFmpeg::Open(const char *path)
{
Close();//打開前先關閉清理
mutex.lock();//鎖
int re = avformat_open_input(&ic, path, 0, 0);//打開解封裝器
if (re != 0)//打開錯誤時
{
mutex.unlock();//解鎖
av_strerror(re, errorbuff, sizeof(errorbuff));//錯誤信息
printf("open %s failed :%s\n", path, errorbuff);
return false;
}
totalMs = ic->duration / (AV_TIME_BASE);//獲取視頻的總時間
printf("file totalSec is %d-%d\n", totalMs/ 60, totalMs % 60);//以分秒計時
mutex.unlock();
return true;
}
void XFFmpeg::Close()
{
mutex.lock();//需要上鎖,以防多線程中你這裏在close,另一個線程中在讀取,
if (ic) avformat_close_input(&ic);//關閉ic上下文
mutex.unlock();
}
std::string XFFmpeg::GetError()
{
mutex.lock();
std::string re = this->errorbuff;
mutex.unlock();
return re;
}
在主函數中這樣調用,用來測試是否正確打開文件
#include "aginexplay.h"
#include "XFFmpeg.h"
#include <QtWidgets/QApplication>
int main(int argc, char *argv[])
{
if (XFFmpeg::Get()->Open("1080.mp4"))//是否打開視頻
{
printf("open success!\n");
}
else
{
printf("open failed!%s\n",XFFmpeg::Get()->GetError().c_str());
getchar();
return -1;
}
QApplication a(argc, argv);
agineXplay w;
w.show();
return a.exec();
二、重構讀取視頻幀接口
封裝讀取視頻幀,我們在XFFMpeg.h中申明
//讀取視頻的每一幀,返回每幀後需要清理空間
AVPacket Read();
在XFFMpeg中定義
AVPacket XFFmpeg::Read()
{
AVPacket pkt;
memset(&pkt,0,sizeof(AVPacket));
mutex.lock();
if (!ic)
{
mutex.unlock();
return pkt;
}
int err = av_read_frame(ic, &pkt);//讀取視頻幀
if (err != 0)//讀取失敗
{
av_strerror(err,errorbuff,sizeof(errorbuff));
}
mutex.unlock();
return pkt;
}
同樣考慮到多線程中,避免在一個線程中剛讀取到一幀視頻,而在另一個線程中就將其清理裏,所以在讀取這一部分也需要加入互斥鎖,讀取完後解鎖。
三、重構解碼接口
在XFFMpeg.h中申明解碼函數以及變量
//讀取到每幀數據後需要對其進行解碼
AVFrame *Decode(const AVPacket *pkt);
AVFrame *yuv = NULL;//解碼後的視頻幀數據
int videoStream = 0;//視頻流
在XFFMpeg.cpp中定義解碼函數
AVFrame * XFFmpeg::Decode(const AVPacket *pkt)
{
mutex.lock();
if (!ic)//若未打開視頻
{
mutex.unlock();
return NULL;
}
if (yuv == NULL)//申請解碼的對象空間
{
yuv = av_frame_alloc();
}
int re = avcodec_send_packet(ic->streams[pkt->stream_index]->codec,pkt);//發送之前讀取的視頻幀pkt
if (re != 0)
{
mutex.unlock();
return NULL;
}
re = avcodec_receive_frame(ic->streams[pkt->stream_index]->codec,yuv);//解碼pkt後存入yuv中
if (re != 0)
{
mutex.unlock();
return NULL;
}
mutex.unlock();
return yuv;
}
同時在解碼時我們需要解碼器所以在XFFMpeg.cpp的Open函數中需要打開解碼器,代碼如下位置
//解碼器
for (int i = 0; i < ic->nb_streams; i++)
{
AVCodecContext *enc = ic->streams[i]->codec;//解碼上下文
if (enc->codec_type == AVMEDIA_TYPE_VIDEO)//判斷是否爲視頻
{
videoStream = i;
//videoCtx = enc;
AVCodec *codec = avcodec_find_decoder(enc->codec_id);//查找解碼器
if (!codec)//未找到解碼器
{
mutex.unlock();
printf("video code not find\n");
return false;
}
int err = avcodec_open2(enc, codec, NULL);//打開解碼器
if (err != 0)//未打開解碼器
{
mutex.unlock();
char buf[1024] = { 0 };
av_strerror(err, buf, sizeof(buf));
printf(buf);
return false;
}
printf("open codec success!\n");
}
}//至此爲打開解碼器過程
printf("file totalSec is %d-%d\n", totalMs/ 60, totalMs % 60);//以分秒計時
mutex.unlock();
return true;
在主函數main中進行測試解碼視頻幀
for (;;)
{
AVPacket pkt = XFFmpeg::Get()->Read();//每次讀取視頻得一幀
if (pkt.size == 0)
break;
printf("pts = %lld\n", pkt.pts);
AVFrame *yuv = XFFmpeg::Get()->Decode(&pkt);//解碼視頻幀
if (yuv)
{
printf("[D]\n");
}
av_packet_unref(&pkt);//重新置pkt爲0
}
同時別忘記在XFFMpeg.cpp的Close時同時還需要釋放解碼後的空間
if (yuv) av_frame_free(&yuv);//關閉時釋放解碼後的視頻幀空間
四、重構ToRGB接口
在XFFMpeg.h中申明轉碼函數以及變量
//將解碼後的YUV視頻幀轉化爲RGB格式
bool ToRGB(const AVFrame *yuv,char *out,int outwidth,int outheight);
SwsContext *cCtx = NULL;//轉碼器上下文
在XFFMpeg.cpp中定義轉碼函數
bool XFFmpeg::ToRGB(const AVFrame *yuv, char *out, int outwidth, int outheight)
{
mutex.lock();
if (!ic)//未打開視頻文件
{
mutex.unlock();
return false;
}
AVCodecContext *videoCtx = ic->streams[this->videoStream]->codec;
cCtx = sws_getCachedContext(cCtx, videoCtx->width,//初始化一個SwsContext
videoCtx->height,
videoCtx->pix_fmt, //輸入像素格式
outwidth, outheight,
AV_PIX_FMT_BGRA,//輸出像素格式
SWS_BICUBIC,//轉碼的算法
NULL, NULL, NULL);
if (!cCtx)
{
mutex.unlock();
printf("sws_getCachedContext failed!\n");
return false;
}
uint8_t *data[AV_NUM_DATA_POINTERS] = { 0 };
data[0] = (uint8_t *)out;//第一位輸出RGB
int linesize[AV_NUM_DATA_POINTERS] = { 0 };
linesize[0] = outwidth * 4;//一行的寬度,32位4個字節
int h = sws_scale(cCtx, yuv->data, //當前處理區域的每個通道數據指針
yuv->linesize,//每個通道行字節數
0, videoCtx->height,//原視頻幀的高度
data,//輸出的每個通道數據指針
linesize//每個通道行字節數
);//開始轉碼
if (h > 0)
{
printf("(%d)", h);
}
mutex.unlock();
return true;
}
在主函數main中進行測試轉碼
for (;;)
{
AVPacket pkt = XFFmpeg::Get()->Read();//每次讀取視頻得一幀
if (pkt.size == 0)
break;
printf("pts = %lld\n", pkt.pts);
if (pkt.stream_index != XFFmpeg::Get()->videoStream)//若不爲視頻流
{
av_packet_unref(&pkt);//重新置pkt爲0
continue;
}
AVFrame *yuv = XFFmpeg::Get()->Decode(&pkt);//解碼視頻幀
if (yuv)
{
printf("[D]\n");
XFFmpeg::Get()->ToRGB(yuv, rgb, 800, 600);//視頻轉碼
}
av_packet_unref(&pkt);//重新置pkt爲0
}
考慮到一個視頻有音頻和視頻,這裏對於音頻我們先直接過濾掉,只處理視頻,同時在XFFMpeg.cpp的Close中對於轉碼上下文的空間也需要釋放。
if (cCtx)
{
sws_freeContext(cCtx);//釋放轉碼器上下文空間
cCtx = NULL;
}
至此FFMPEG視頻處理原理以及實現基本完成了,下一篇對於QT界面設計和使用opengl繪製視頻的總結。