上一篇中解封裝之后能得到每一幀的數(shù)據(jù)罢绽,這個(gè)數(shù)據(jù)如果是原始數(shù)據(jù)沒有編碼的畏线,那么可以直接使用,音頻和視頻都是良价,但是往往都編碼過(guò)的寝殴,不然數(shù)據(jù)量太大了,所以數(shù)據(jù)的解碼就不可缺少了明垢。
解碼
解碼一般分成以下幾步:
準(zhǔn)備工作
因?yàn)槌跏蓟獯a器需要解封裝提供解碼器id杯矩,發(fā)送數(shù)據(jù)包需要解封裝的幀數(shù)據(jù),所以需要保存解封裝中音視頻信息的參數(shù)以及解封裝之后的幀數(shù)據(jù)袖外。
這里我們創(chuàng)建一個(gè)類去保存參數(shù)信息,雖然目前就一個(gè)值魂务,AVCodecParameters曼验,但是之后音頻還需要通道數(shù)和采樣率,所以先把參數(shù)類封裝一下粘姜,之后就添加屬性就好了鬓照,例如叫做FFPrameters吧,如下:
struct AVCodecParameters;
class FFParameters {
public:
AVCodecParameters *params = 0;
};
然后修改Demux中的獲取音視頻參數(shù)的方法孤紧。核心方法就是params.params = ic->streams[re]->codecpar;
其中re表示音頻或視頻流的索引豺裆。整體方法修改如下:
定義:
virtual FFParameters getVideoParams();
virtual FFParameters getAudioParams();
實(shí)現(xiàn):
FFParameters Demux::getVideoParams() {
if (!ic) {
return FFParameters();
}
int re = av_find_best_stream(ic, AVMEDIA_TYPE_VIDEO, -1, -1, 0, 0);
if (re < 0) {
LOG_E("av_find_best_stream video failed");
return FFParameters();
}
videoStream = re;
FFParameters params;
params.params = ic->streams[re]->codecpar;
return params;
}
FFParameters Demux::getAudioParams() {
if (!ic) {
return FFParameters();
}
int re = av_find_best_stream(ic, AVMEDIA_TYPE_AUDIO, -1, -1, 0, 0);
if (re < 0) {
LOG_E("av_find_best_stream audio failed");
return FFParameters();
}
audioStream = re;
FFParameters params;
params.params = ic->streams[re]->codecpar;
return params;
}
然后幀數(shù)據(jù)可以定義一個(gè)FFData類,用來(lái)存儲(chǔ)讀取出來(lái)的每一幀數(shù)據(jù)号显,因?yàn)樵诮獯a時(shí)會(huì)用到解封裝出來(lái)的幀數(shù)據(jù)臭猜,然后還需要保存這個(gè)數(shù)據(jù)是音頻還是視頻。如下:
class FFData {
public:
bool isAudio = false;
//保存解封裝packet和解碼frame的數(shù)據(jù)
unsigned char *data = 0;
}
然后我們還需要在解封裝的部分押蚤,把解封裝的數(shù)據(jù)返回蔑歌,以便解碼時(shí)能獲取到,也就是修改解封裝的read方法揽碘,修改后如下:
FFData Demux::read() {
if (!ic) {
return FFData();
}
AVPacket *pkt = av_packet_alloc();
int re = av_read_frame(ic, pkt);
if (re != 0) {
av_packet_free(&pkt);
return FFData();
}
FFData data;
data.data = (unsigned char *) pkt;
pkt->pts = (long long) (pkt->pts * (1000 * r2d(ic->streams[pkt->stream_index]->time_base)));
if (pkt->stream_index == audioStream) {
data.isAudio = true;
// LOG_I("read audio size = %d,pts = %lld", pkt->size, pkt->pts);
} else if (pkt->stream_index == videoStream) {
data.isAudio = false;
// LOG_I("read video size = %d,pts = %lld", pkt->size, pkt->pts);
} else {
av_packet_free(&pkt);
return FFData();
}
return data;
}
其中返回?cái)?shù)據(jù)類型變了次屠,然后在返回之前先不把解封裝數(shù)據(jù)清理掉园匹,這一步會(huì)留到解碼完這一幀之后去清理,所以我們的FFData類還需要增加一個(gè)清理的方法:
extern "C" {
#include <libavcodec/avcodec.h>
}
void FFData::clear() {
if (!data) {
return;
}
av_packet_free((AVPacket **) &data);
data = 0;
}
說(shuō)到清理劫灶,我們?cè)诮夥庋b完成后裸违,也應(yīng)該關(guān)閉解封裝上下文,所以在Demux中增加:
void Demux::close() {
if (ic) {
avformat_close_input(&ic);
}
}
然后再在cpp文件下創(chuàng)建Decode類本昏,并在CMakeLists中申明供汛,當(dāng)然前邊新定義的FFParameters和FFData都需要在CMakeLists中申明。解碼按照流程圖可以定義三個(gè)方法:初始化凛俱,發(fā)送數(shù)據(jù)包紊馏,接收數(shù)據(jù)包,如下:
public:
virtual void init(FFParameters params);
virtual void sendPacket(FFData data);
virtual FFData receivePacket();
這樣準(zhǔn)備工作基本完成蒲犬。
初始化解碼器
初始化解碼器朱监,又可以分為:
- 查找解碼器
- 創(chuàng)建解碼上下文,并復(fù)制參數(shù)
- 打開解碼器
對(duì)應(yīng)著幾個(gè)核心方法:
AVCodec *avcodec_find_decoder(enum AVCodecID id);//查找解碼器
AVCodecContext *avcodec_alloc_context3(const AVCodec *codec);//創(chuàng)建解碼器上下文
int avcodec_parameters_to_context(AVCodecContext *codec,
const AVCodecParameters *par);//復(fù)制參數(shù)到解碼器上下文
int avcodec_open2(AVCodecContext *avctx, const AVCodec *codec, AVDictionary **options);//打開解碼器
其中我們解碼器上下文會(huì)在發(fā)送數(shù)據(jù)包和接收數(shù)據(jù)包的時(shí)候也會(huì)用到原叮,所以會(huì)把它作為屬性赫编。
這三個(gè)方法的具體實(shí)現(xiàn)如下:
void Decode::init(FFParameters params) {
avcodec_register_all();
if (!params.params) {
LOG_E("Decode init params is empty");
return;
}
AVCodecParameters *p = params.params;
//查找解碼器
AVCodec *codec = avcodec_find_decoder(p->codec_id);
if (!codec) {
LOG_E("avcodec_find_decoder %d failed", p->codec_id);
return;
}
LOG_I("avcodec_find_decoder %d success", p->codec_id);
//創(chuàng)建解碼器上下文,并復(fù)制參數(shù)
codecContext = avcodec_alloc_context3(codec);
avcodec_parameters_to_context(codecContext, p);
//解碼線程數(shù)量
codecContext->thread_count = 8;
//打開解碼器
int re = avcodec_open2(codecContext, 0, 0);
if (re != 0) {
char buff[1024] = {0};
av_strerror(re, buff, sizeof(buff));
LOG_E("%s", buff);
return;
}
LOG_I("avcodec_open2 success");
}
這樣就初始化好解碼器了奋隶。
發(fā)送數(shù)據(jù)包
這一步就是使用解封裝得到的包數(shù)據(jù)擂送,發(fā)送給解碼隊(duì)列即可,核心方法就是avcodec_send_packet
唯欣,參數(shù)需要解碼器上下文和包數(shù)據(jù)嘹吨,實(shí)現(xiàn)如下:
void Decode::sendPacket(FFData data) {
if (!data.data) {
return;
}
if (!codecContext) {
return;
}
int re = avcodec_send_packet(codecContext, (AVPacket *) data.data);
if (re != 0) {
LOG_E("avcodec_send_packet failed");
return;
}
}
這樣發(fā)送數(shù)據(jù)包就完成了。
接收數(shù)據(jù)包
這一步需要注意一點(diǎn)就是發(fā)送一個(gè)數(shù)據(jù)包給解碼器隊(duì)列境氢,可能需要調(diào)用多次接收數(shù)據(jù)包才能獲取完成蟀拷,核心函數(shù)是avcodec_receive_frame
,參數(shù)是解碼器上下文和解碼后的到的幀數(shù)據(jù)萍聊,這個(gè)幀數(shù)據(jù)问芬,需要手動(dòng)分配空間,當(dāng)然也需要手動(dòng)清理空間寿桨,清理稍后再說(shuō)此衅,因?yàn)閹瑪?shù)據(jù)每次都會(huì)覆蓋上一次的數(shù)據(jù),所以可以重復(fù)利用亭螟,沒必要每次都申請(qǐng)空間挡鞍,所以可以作為屬性,具體實(shí)現(xiàn)如下:
FFData Decode::receivePacket() {
if (!codecContext) {
return FFData();
}
if (!frame) {
frame = av_frame_alloc();
}
int re = avcodec_receive_frame(codecContext, frame);
if (re != 0) {
return FFData();
}
FFData data;
data.data = (unsigned char *) frame;
data.format = frame->format;
data.pts = frame->pts;
memcpy(data.decodeData, frame->data, sizeof(data.decodeData));
if (codecContext->codec_type == AVMEDIA_TYPE_VIDEO) {
data.size = (frame->linesize[0] + frame->linesize[1] + frame->linesize[2]) * frame->height;
data.width = frame->width;
data.height = frame->height;
} else {
data.size = av_get_bytes_per_sample((AVSampleFormat) frame->format) * frame->nb_samples +
frame->channels;
}
LOG_I("receive frame data size = %d,pts = %lld", data.size, data.pts);
return data;
}
可以注意到预烙,F(xiàn)FData再次多添加了一些屬性匕累,用來(lái)存儲(chǔ)解碼之后的數(shù)據(jù),包括數(shù)據(jù)類型默伍,pts欢嘿,解碼數(shù)據(jù)衰琐,數(shù)據(jù)大小,視頻數(shù)據(jù)的寬高等炼蹦。因?yàn)檫@在之后的音視頻顯示和播放中會(huì)使用到羡宙。其中frame是接收到的解碼數(shù)據(jù),可以重復(fù)使用掐隐,所以可以作為解碼的屬性狗热,在最后的清理中再清除空間。
其中數(shù)據(jù)的大小計(jì)算方式音頻和視頻不一樣虑省,而且即使這樣算出來(lái)的大小也可能有錯(cuò)匿刮,因?yàn)橐曨l數(shù)據(jù)對(duì)齊也很關(guān)鍵,會(huì)在之后的適配中處理不同視頻類型的數(shù)據(jù)對(duì)齊問題探颈。
最后也需要在解碼完把數(shù)據(jù)清理:
void Decode::close() {
if (frame) {
av_frame_free(&frame);
}
if (codecContext) {
avcodec_flush_buffers(codecContext);
avcodec_close(codecContext);
avcodec_free_context(&codecContext);
}
}
這樣解碼部分的代碼也基本完成熟丸,然后再把解碼和解封裝聯(lián)系起來(lái)。
關(guān)聯(lián)解封裝和解碼
關(guān)聯(lián)部分伪节,暫時(shí)為了方便還是在native-lib.cpp文件中寫光羞,修改如下:
void decodeData(Decode *decode, FFData data);
extern "C"
JNIEXPORT void JNICALL
Java_net_arvin_ffmpegtest_FFmpegUtil_init(JNIEnv *env, jclass type) {
if (!demux) {
demux = new Demux();
demux->init();
}
}
extern "C"
JNIEXPORT void JNICALL
Java_net_arvin_ffmpegtest_FFmpegUtil_open(JNIEnv *env, jclass type, jstring url_) {
const char *url = env->GetStringUTFChars(url_, 0);
if (demux) {
demux->open(url);
}
if (!audioDecode) {
audioDecode = new Decode();
audioDecode->init(demux->getAudioParams());
}
if (!videoDecode) {
videoDecode = new Decode();
videoDecode->init(demux->getVideoParams());
}
env->ReleaseStringUTFChars(url_, url);
}
extern "C"
JNIEXPORT void JNICALL
Java_net_arvin_ffmpegtest_FFmpegUtil_read(JNIEnv *env, jclass type) {
if (!demux) {
return;
}
bool re = true;
while (re) {
FFData data = demux->read();
re = data.data != 0;
if (re) {
if (data.isAudio) {
decodeData(audioDecode, data);
} else {
decodeData(videoDecode, data);
}
}
}
}
void decodeData(Decode *decode, FFData data) {
if (!decode) {
return;
}
decode->sendPacket(data);
while (true) {
FFData frame = decode->receivePacket();
if (frame.data == 0) {
break;
}
}
data.clear();
}
extern "C"
JNIEXPORT void JNICALL
Java_net_arvin_ffmpegtest_FFmpegUtil_close(JNIEnv *env, jclass type) {
if (demux) {
demux->close();
}
if (audioDecode) {
audioDecode->close();
}
if (videoDecode) {
videoDecode->close();
}
}
這部分是我的native-lib的代碼,注意不要直接全部拷貝過(guò)去怀大,因?yàn)閷?duì)應(yīng)的類名不一致纱兑,會(huì)找不到方法的。其中在解封裝打開完成之后化借,初始化解碼器潜慎,然后在解封裝讀取到數(shù)據(jù)的時(shí)候,判斷音頻還是視頻交給不同的解碼器去處理數(shù)據(jù)蓖康,具體數(shù)據(jù)的打印在解碼器的接收數(shù)據(jù)函數(shù)中勘纯。
然后在界面中,需要新加一個(gè)清除資源按鈕钓瞭,然后在FFmpegUtil中增加一個(gè)close方法,點(diǎn)擊清楚資源按鈕時(shí)調(diào)用FFmpegUtil的close方法淫奔。
這樣解碼部分也就基本完成山涡。