視頻播放器之解碼

上一篇中解封裝之后能得到每一幀的數(shù)據(jù)罢绽,這個(gè)數(shù)據(jù)如果是原始數(shù)據(jù)沒有編碼的畏线,那么可以直接使用,音頻和視頻都是良价,但是往往都編碼過(guò)的寝殴,不然數(shù)據(jù)量太大了,所以數(shù)據(jù)的解碼就不可缺少了明垢。

解碼

解碼一般分成以下幾步:

解碼.jpg

準(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方法淫奔。

這樣解碼部分也就基本完成山涡。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市唆迁,隨后出現(xiàn)的幾起案子鸭丛,更是在濱河造成了極大的恐慌,老刑警劉巖唐责,帶你破解...
    沈念sama閱讀 218,036評(píng)論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件鳞溉,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡鼠哥,警方通過(guò)查閱死者的電腦和手機(jī)熟菲,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,046評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門看政,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人抄罕,你說(shuō)我怎么就攤上這事允蚣。” “怎么了呆贿?”我有些...
    開封第一講書人閱讀 164,411評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵嚷兔,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我做入,道長(zhǎng)冒晰,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,622評(píng)論 1 293
  • 正文 為了忘掉前任竟块,我火速辦了婚禮壶运,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘彩郊。我一直安慰自己前弯,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,661評(píng)論 6 392
  • 文/花漫 我一把揭開白布秫逝。 她就那樣靜靜地躺著恕出,像睡著了一般。 火紅的嫁衣襯著肌膚如雪违帆。 梳的紋絲不亂的頭發(fā)上浙巫,一...
    開封第一講書人閱讀 51,521評(píng)論 1 304
  • 那天,我揣著相機(jī)與錄音刷后,去河邊找鬼的畴。 笑死,一個(gè)胖子當(dāng)著我的面吹牛尝胆,可吹牛的內(nèi)容都是我干的丧裁。 我是一名探鬼主播,決...
    沈念sama閱讀 40,288評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼含衔,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼煎娇!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起贪染,我...
    開封第一講書人閱讀 39,200評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤缓呛,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后杭隙,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體哟绊,經(jīng)...
    沈念sama閱讀 45,644評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,837評(píng)論 3 336
  • 正文 我和宋清朗相戀三年痰憎,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了票髓。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片攀涵。...
    茶點(diǎn)故事閱讀 39,953評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖炬称,靈堂內(nèi)的尸體忽然破棺而出汁果,到底是詐尸還是另有隱情,我是刑警寧澤玲躯,帶...
    沈念sama閱讀 35,673評(píng)論 5 346
  • 正文 年R本政府宣布据德,位于F島的核電站,受9級(jí)特大地震影響跷车,放射性物質(zhì)發(fā)生泄漏棘利。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,281評(píng)論 3 329
  • 文/蒙蒙 一朽缴、第九天 我趴在偏房一處隱蔽的房頂上張望善玫。 院中可真熱鬧,春花似錦密强、人聲如沸茅郎。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,889評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)系冗。三九已至,卻和暖如春薪鹦,著一層夾襖步出監(jiān)牢的瞬間掌敬,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,011評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工池磁, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留奔害,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,119評(píng)論 3 370
  • 正文 我出身青樓地熄,卻偏偏與公主長(zhǎng)得像华临,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子端考,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,901評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容