FFmpeg視頻解碼播放

一、FFmpeg 相關(guān)庫簡介

介紹
avcodec 音視頻編解碼核心庫
avformat 音視頻容器格式的封裝和解析
avutil 核心工具庫
swscal 圖像格式轉(zhuǎn)換的模塊
swresampel 音頻重采樣
avfilter 音視頻濾鏡庫 如視頻加水印、音頻變聲
avdevice 輸入輸出設(shè)備庫,提供設(shè)備數(shù)據(jù)的輸入與輸出

FFmpeg 就是依靠以上幾個(gè)庫,實(shí)現(xiàn)了強(qiáng)大的音視頻編碼酣衷、解碼、編輯、轉(zhuǎn)換开呐、采集等能力。

二规求、FFMpeg 解碼流程簡介

在前面的系列文章中筐付,利用了 Android 提供的原生硬解碼能力,使用實(shí)現(xiàn)了視頻的解碼和播放阻肿。

總結(jié)起來有以下的流程:

  • 初始化解碼器
  • 讀取 Mp4 文件中的編碼數(shù)據(jù)瓦戚,并送入解碼器解碼
  • 獲取解碼好的幀數(shù)據(jù)
  • 將一幀畫面渲染到屏幕上

FFmpeg 解碼無非也就是以上過程,只不過 FFmpeg 是利用 CPU 的計(jì)算能力來解碼而已丛塌。

1. FFmpeg 初始化

FFmpeg 初始化的流程相對 Android 原生硬解碼來說還是比較瑣碎的较解,但是流程都是固定的,一旦封裝起來就可以直接套用了赴邻。

首先來看一下初始化的流程圖

image

其實(shí)就是根據(jù)待解碼文件的格式,進(jìn)行一系列參數(shù)的初始化姥敛。

其中奸焙,有幾個(gè) 結(jié)構(gòu)體 比較重要,分別是 AVFormatContext(format_ctx)彤敛、AVCodecContext(codec_ctx)忿偷、AVCodec(codec)

結(jié)構(gòu)體 :FFmpeg 是基于 C 語言開發(fā)的,我們知道 C 語言是面向過程的語言臊泌,也就是說不像 C++ 有類來封裝內(nèi)部數(shù)據(jù)鲤桥。但是 C 提供了結(jié)構(gòu)體,可以用來實(shí)現(xiàn)數(shù)據(jù)的封裝渠概,達(dá)到類似于類的效果茶凳。

  • AVFormatContext:隸屬于 avformat 庫嫂拴,存放這碼流數(shù)據(jù)的上下文,主要用于音視頻的 封裝解封贮喧。

  • AVCodecContext:隸屬于 avcodec 庫筒狠,存放編解碼器參數(shù)上下文,主要用于對音視頻數(shù)據(jù)進(jìn)行 編碼解碼箱沦。

  • AVCodec:隸屬于 avcodec 庫辩恼,音視頻編解碼器,真正編解碼執(zhí)行者谓形。

2. FFmpeg 解碼循環(huán)

同樣的灶伊,通過一個(gè)流程圖來說明具體解碼過程:

image

在初始化完 FFmpeg 后,就可以進(jìn)行具體的數(shù)據(jù)幀解碼了寒跳。

從上圖可以看到聘萨,FFmpeg 首先將數(shù)據(jù)提取為一個(gè) AVPacket(avpacket),然后通過解碼童太,將數(shù)據(jù)解碼為一幀可以渲染的數(shù)據(jù)米辐,稱為 AVFrame(frame)。

同樣的书释,AVPacketAVFrame 也是兩個(gè)結(jié)構(gòu)體翘贮,里面封裝了具體的數(shù)據(jù)。

三爆惧、封裝解碼類

有了以上對解碼流程的了解狸页,就可以根據(jù)上面的 流程圖 來編寫代碼了。

根據(jù)以往的經(jīng)驗(yàn)检激,既然 FFmepg 的初始化和解碼流程都是一些瑣碎重復(fù)的工作肴捉,那么我們必然是要對其進(jìn)行封裝的腹侣,以便更好的復(fù)用和拓展叔收。

解碼流程封裝

1. 定義解碼狀態(tài): decode_state.h

src/main/cpp/media/decoder 目錄上,右鍵 New -> C++ Header File傲隶,輸入 decode_state

//decode_state.h

#ifndef LEARNVIDEO_DECODESTATE_H
#define LEARNVIDEO_DECODESTATE_H

enum DecodeState {
    STOP,
    PREPARE,
    START,
    DECODING,
    PAUSE,
    FINISH
};

#endif //LEARNVIDEO_DECODESTATE_H

這是一個(gè)枚舉饺律,定義了解碼器解碼的狀態(tài)

2. 定義解碼器的基礎(chǔ)功能:i_decoder.h:

src/main/cpp/media/decoder 目錄上,右鍵 New -> C++ Header File跺株,輸入 i_decoder复濒。

// i_decoder.h

#ifndef LEARNVIDEO_I_DECODER_H
#define LEARNVIDEO_I_DECODER_H

class IDecoder {
public:
    virtual void GoOn() = 0;
    virtual void Pause() = 0;
    virtual void Stop() = 0;
    virtual bool IsRunning() = 0;
    virtual long GetDuration() = 0;
    virtual long GetCurPos() = 0;
};

這是一個(gè)純虛類,類似 Javainterface(具體可查看 Android NDK入門:C++ 基礎(chǔ)知識)乒省,定義了解碼器該有的基礎(chǔ)方法巧颈。

3. 定義一個(gè)解碼器基礎(chǔ)類 base_decoder

src/main/cpp/media/decoder 目錄上袖扛,右鍵 New -> C++ Class 輸入 base_decoder 砸泛,該類用于封裝解碼中最基礎(chǔ)的流程十籍。

會生成兩個(gè)文件:base_decoder.hbase_decoder.cpp唇礁。

  • 定義頭文件:base_decoder.h

//base_decoder.h

#ifndef LEARNVIDEO_BASEDECODER_H
#define LEARNVIDEO_BASEDECODER_H

#include <jni.h>
#include <string>
#include <thread>
#include "../../utils/logger.h"
#include "i_decoder.h"
#include "decode_state.h"

extern "C" {
#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include <libavutil/frame.h>
#include <libavutil/time.h>
};

class BaseDecoder: public IDecoder {

private:

    const char *TAG = "BaseDecoder";

    //-------------定義解碼相關(guān)------------------------------
    // 解碼信息上下文
    AVFormatContext *m_format_ctx = NULL;

    // 解碼器
    AVCodec *m_codec = NULL;

    // 解碼器上下文
    AVCodecContext *m_codec_ctx = NULL;

    // 待解碼包
    AVPacket *m_packet = NULL;

    // 最終解碼數(shù)據(jù)
    AVFrame *m_frame = NULL;

    // 當(dāng)前播放時(shí)間
    int64_t m_cur_t_s = 0;

    // 總時(shí)長
    long m_duration = 0;

    // 開始播放的時(shí)間
    int64_t m_started_t = -1;

    // 解碼狀態(tài)
    DecodeState m_state = STOP;

    // 數(shù)據(jù)流索引
    int m_stream_index = -1;

    // 省略其他

    // ......

}

注意:在引入 FFmpeg 相關(guān)庫的頭文件時(shí)勾栗,需要注意把 #include 放到 extern "C" {} 中。因?yàn)?FFmpegC 語言寫的盏筐,所以在引入到 C++ 文件中的時(shí)候围俘,需要標(biāo)記以 C 的方式來編譯,否則會導(dǎo)致編譯出錯(cuò)琢融。

在頭文件中界牡,先聲明在 cpp 需要用到的相關(guān)變量,重點(diǎn)就是上一節(jié)提到的幾個(gè)解碼相關(guān)的結(jié)構(gòu)體吏奸。

  • 定義初始化和解碼循環(huán)相關(guān)的方法:

//base_decoder.h

class BaseDecoder: public IDecoder {

private:

    const char *TAG = "BaseDecoder";

    //-------------定義解碼相關(guān)------------------------------
    //省略....

    //-----------------私有方法------------------------------

    /**
     * 初始化FFMpeg相關(guān)的參數(shù)
     * @param env jvm環(huán)境
     */
    void InitFFMpegDecoder(JNIEnv * env);

    /**
     * 分配解碼過程中需要的緩存
     */
    void AllocFrameBuffer();

    /**
     * 循環(huán)解碼
     */
    void LoopDecode();

    /**
     * 獲取當(dāng)前幀時(shí)間戳
     */
    void ObtainTimeStamp();

    /**
     * 解碼完成
     * @param env jvm環(huán)境
     */
    void DoneDecode(JNIEnv *env);

    /**
     * 時(shí)間同步
     */
    void SyncRender();

    // 省略其他

    // ......

}

  • 這個(gè)解碼基礎(chǔ)類繼承自 i_decoder欢揖,還需要實(shí)現(xiàn)其中規(guī)定的通用方法。

//base_decoder.h

class BaseDecoder: public IDecoder {

    //省略其他

    //......

public:

    //--------構(gòu)造方法和析構(gòu)方法-------------

    BaseDecoder(JNIEnv *env, jstring path);
    virtual ~BaseDecoder();

    //--------實(shí)現(xiàn)基礎(chǔ)類方法-----------------

    void GoOn() override;
    void Pause() override;
    void Stop() override;
    bool IsRunning() override;
    long GetDuration() override;
    long GetCurPos() override;
}

  • 定義解碼線程

我們知道奋蔚,解碼是一個(gè)非常耗時(shí)的操作她混,就像原生硬解一樣,我們需要開啟一個(gè)線程來承載解碼任務(wù)泊碑。所以坤按,先在頭文件中定義好線程相關(guān)的變量和方法。

//base_decoder.h

class BaseDecoder: public IDecoder {

private:

    //省略其他

    //......

    // -------------------定義線程相關(guān)-----------------------------
    // 線程依附的JVM環(huán)境
    JavaVM *m_jvm_for_thread = NULL;

    // 原始路徑j(luò)string引用,否則無法在線程中操作
    jobject m_path_ref = NULL;

    // 經(jīng)過轉(zhuǎn)換的路徑
    const char *m_path = NULL;

    // 線程等待鎖變量
    pthread_mutex_t m_mutex = PTHREAD_MUTEX_INITIALIZER;
    pthread_cond_t m_cond = PTHREAD_COND_INITIALIZER;

    /**
     * 新建解碼線程
     */
    void CreateDecodeThread();

    /**
     * 靜態(tài)解碼方法喇勋,用于解碼線程回調(diào)
     * @param that 當(dāng)前解碼器
     */
    static void Decode(std::shared_ptr<BaseDecoder> that);

protected:

    /**
     * 進(jìn)入等待
     */
    void Wait(long second = 0);

    /**
     * 恢復(fù)解碼
     */
    void SendSignal();

}

  • 定義子類需要實(shí)現(xiàn)的虛函數(shù)
//base_decoder.h

class BaseDecoder: public IDecoder {
protected:

    /**
     * 子類準(zhǔn)備回調(diào)方法
     * @note 注:在解碼線程中回調(diào)
     * @param env 解碼線程綁定的JVM環(huán)境
     */
    virtual void Prepare(JNIEnv *env) = 0;

    /**
     * 子類渲染回調(diào)方法
     * @note 注:在解碼線程中回調(diào)
     * @param frame 視頻:一幀YUV數(shù)據(jù)尸昧;音頻:一幀PCM數(shù)據(jù)
     */
    virtual void Render(AVFrame *frame) = 0;

    /**
     * 子類釋放資源回調(diào)方法
     */
    virtual void Release() = 0;

}

以上,就定義好了解碼類的基礎(chǔ)結(jié)構(gòu):

  • FFmpeg 解碼相關(guān)的結(jié)構(gòu)體參數(shù)
  • 解碼器基本方法
  • 解碼線程
  • 規(guī)定子類需要實(shí)現(xiàn)的方法
4. 實(shí)現(xiàn)基礎(chǔ)解碼器

base_decoder.cpp 中来累,實(shí)現(xiàn)頭文件中聲明的方法

  • 初始化解碼線程

// base_decoder.cpp

#include "base_decoder.h"
#include "../../utils/timer.c"

BaseDecoder::BaseDecoder(JNIEnv *env, jstring path) {
    Init(env, path);
    CreateDecodeThread();
}

BaseDecoder::~BaseDecoder() {
    if (m_format_ctx != NULL) delete m_format_ctx;
    if (m_codec_ctx != NULL) delete m_codec_ctx;
    if (m_frame != NULL) delete m_frame;
    if (m_packet != NULL) delete m_packet;
}

void BaseDecoder::Init(JNIEnv *env, jstring path) {
    m_path_ref = env->NewGlobalRef(path);
    m_path = env->GetStringUTFChars(path, NULL);
    //獲取JVM虛擬機(jī),為創(chuàng)建線程作準(zhǔn)備
    env->GetJavaVM(&m_jvm_for_thread);
}

void BaseDecoder::CreateDecodeThread() {
    // 使用智能指針窘奏,線程結(jié)束時(shí)嘹锁,自動刪除本類指針
    std::shared_ptr<BaseDecoder> that(this);
    std::thread t(Decode, that);
    t.detach();
}

構(gòu)造函數(shù)很簡單,傳入 JNI 環(huán)境變量着裹,以及待解碼文件路徑领猾。

Init 方法中,因?yàn)?jstring 并非 C++ 的標(biāo)準(zhǔn)類型骇扇,需要將 jstring 類型的 path 轉(zhuǎn)換為 char 類型摔竿,才能使用。

說明:由于 JNIEnv線程 是一一對應(yīng)的少孝,也就是說继低,在 Android 中,JNI環(huán)境 是和線程綁定的稍走,每一個(gè)線程都有一個(gè)獨(dú)立的 JNIEnv 環(huán)境袁翁,并且互相之間不可訪問冷溃。所以如果要在新的線程中訪問 JNIEnv,需要為這個(gè)線程創(chuàng)建一個(gè)新的 JNIEnv 梦裂。

Init 方法的最后似枕,通過 env->GetJavaVM(&m_jvm_for_thread) 獲取到 JavaVM 實(shí)例,保存到 m_jvm_for_thread年柠,該實(shí)例是所有共享的 凿歼,通過它就可以為解碼線程獲取一個(gè)新的 JNIEnv 環(huán)境。

C++ 中創(chuàng)建線程非常簡單冗恨,只需兩句話答憔,就可以啟動一個(gè)線程:

std::thread t(靜態(tài)方法, 靜態(tài)方法參數(shù));
t.detach();

也就是說,這個(gè)線程需要一個(gè)靜態(tài)方法作為參數(shù)掀抹,啟動以后虐拓,會回調(diào)這個(gè)靜態(tài)方法,并且可以給這個(gè)靜態(tài)方法傳遞參數(shù)傲武。

另外蓉驹,CreateDecodeThread 方法中的第一代碼,是用于創(chuàng)建一個(gè)智能指針揪利。

我們知道态兴, C++ new 出來的指針對象是需要我們手動 delete 刪除的疟位,否則就會出現(xiàn)內(nèi)存泄漏瞻润。而智能指針的作用就是幫我們實(shí)現(xiàn)內(nèi)存管理。

當(dāng)這個(gè)指針的引用計(jì)數(shù)為 0 時(shí)甜刻,就會自動銷毀绍撞。也就是說,不需要我們自己去手動 delete 得院。

std::shared_ptr<BaseDecoder> that(this);

這里將 this 封裝成名為 that 的智能指針傻铣,那么在外部使用解碼器的時(shí)候,就不需要手動釋放內(nèi)存了尿招,當(dāng)解碼線程退出的時(shí)候矾柜,會自動銷毀阱驾,并調(diào)用析構(gòu)函數(shù)就谜。

  • 封裝解碼流程
// base_decoder.cpp

void BaseDecoder::Decode(std::shared_ptr<BaseDecoder> that) {
    JNIEnv * env;

    //將線程附加到虛擬機(jī),并獲取env
    if (that->m_jvm_for_thread->AttachCurrentThread(&env, NULL) != JNI_OK) {
        LOG_ERROR(that->TAG, that->LogSpec(), "Fail to Init decode thread");
        return;
    }

    // 初始化解碼器
    that->InitFFMpegDecoder(env);
    // 分配解碼幀數(shù)據(jù)內(nèi)存
    that->AllocFrameBuffer();
    // 回調(diào)子類方法里覆,通知子類解碼器初始化完畢
    that->Prepare(env);
    // 進(jìn)入解碼循環(huán)
    that->LoopDecode();
    // 退出解碼
    that->DoneDecode(env);

    //解除線程和jvm關(guān)聯(lián)
    that->m_jvm_for_thread->DetachCurrentThread();

}

base_decoder.h 頭文件聲明中丧荐, Decode 是一個(gè)靜態(tài)的成員方法。

首先為解碼線程創(chuàng)建了 JNIEnv 喧枷,失敗則直接退出解碼虹统。

以上 Decode 方法中就是分步調(diào)用對應(yīng)的方法弓坞,很簡單,看注釋即可车荔。

接下來看具體的分步調(diào)用的內(nèi)容渡冻。

  • 初始化解碼器
void BaseDecoder::InitFFMpegDecoder(JNIEnv * env) {
    //1,初始化上下文
    m_format_ctx = avformat_alloc_context();

    //2忧便,打開文件
    if (avformat_open_input(&m_format_ctx, m_path, NULL, NULL) != 0) {
        LOG_ERROR(TAG, LogSpec(), "Fail to open file [%s]", m_path);
        DoneDecode(env);
        return;
    }

    //3族吻,獲取音視頻流信息
    if (avformat_find_stream_info(m_format_ctx, NULL) < 0) {
        LOG_ERROR(TAG, LogSpec(), "Fail to find stream info");
        DoneDecode(env);
        return;
    }

    //4,查找編解碼器
    //4.1 獲取視頻流的索引
    int vIdx = -1;//存放視頻流的索引
    for (int i = 0; i < m_format_ctx->nb_streams; ++i) {
        if (m_format_ctx->streams[i]->codecpar->codec_type == GetMediaType()) {
            vIdx = i;
            break;
        }
    }
    if (vIdx == -1) {
        LOG_ERROR(TAG, LogSpec(), "Fail to find stream index")
        DoneDecode(env);
        return;
    }
    m_stream_index = vIdx;

    //4.2 獲取解碼器參數(shù)
    AVCodecParameters *codecPar = m_format_ctx->streams[vIdx]->codecpar;

    //4.3 獲取解碼器
    m_codec = avcodec_find_decoder(codecPar->codec_id);

    //4.4 獲取解碼器上下文
    m_codec_ctx = avcodec_alloc_context3(m_codec);
    if (avcodec_parameters_to_context(m_codec_ctx, codecPar) != 0) {
        LOG_ERROR(TAG, LogSpec(), "Fail to obtain av codec context");
        DoneDecode(env);
        return;
    }

    //5珠增,打開解碼器
    if (avcodec_open2(m_codec_ctx, m_codec, NULL) < 0) {
        LOG_ERROR(TAG, LogSpec(), "Fail to open av codec");
        DoneDecode(env);
        return;
    }

    m_duration = (long)((float)m_format_ctx->duration/AV_TIME_BASE * 1000);

    LOG_INFO(TAG, LogSpec(), "Decoder init success")
}

看起來好像很復(fù)雜超歌,實(shí)際上套路都是一樣的,一開始看會感到不適應(yīng)蒂教,主要是因?yàn)檫@些方法是面向過程的調(diào)用方法巍举,和平時(shí)使用的面向?qū)ο笳Z言使用習(xí)慣不太一樣。

舉個(gè)例子:

上面代碼中凝垛,打開文件的方法是這樣的:

avformat_open_input(&m_format_ctx, m_path, NULL, NULL);

而如果是面向?qū)ο蟮脑挵妹酰a通常是這樣的:

// 注意:以下為偽代碼,僅用于舉例說明

m_format_ctx.avformat_open_input(m_path);

那么怎么理解 C 中的這種面向過程的調(diào)用呢梦皮?

我們知道 m_format_ctx 是結(jié)構(gòu)體定枷,封裝了具體的數(shù)據(jù),那么 avformat_open_input 這個(gè)方法其實(shí)就是操作這個(gè)結(jié)構(gòu)體的方法届氢,不同的方法調(diào)用欠窒,是對結(jié)構(gòu)體中不同數(shù)據(jù)的操作。

具體流程請看上面的注釋退子,不在細(xì)說岖妄,其實(shí)就是第一節(jié)中 【初始化流程圖】 中步驟的實(shí)現(xiàn)。

有兩點(diǎn)需要注意的:

  1. FFmpeg 中帶有 alloc 字樣的方法寂祥,通常只是初始化對應(yīng)的結(jié)構(gòu)體荐虐,但是具體的參數(shù)和數(shù)據(jù)緩存區(qū),一般都要經(jīng)過另外方法的初始化才能使用丸凭,

比如 m_format_ctx, m_codec_ctx

// 創(chuàng)建
m_format_ctx = avformat_alloc_context();
// 初始化流信息
avformat_open_input(&m_format_ctx, m_path, NULL, NULL)

-------------------------------------------------------

// 創(chuàng)建
m_codec_ctx = avcodec_alloc_context3(m_codec);
//初始化具體內(nèi)容
avcodec_parameters_to_context(m_codec_ctx, codecPar);

  1. 關(guān)于代碼中注釋的第 4 點(diǎn)

我們知道音視頻數(shù)據(jù)通常封裝在不同的軌道中福扬,所以,要想獲取到正確的音視頻數(shù)據(jù)惜犀,就需要先獲取到對應(yīng)的索引铛碑。

音視頻的數(shù)據(jù)類型,通過虛函數(shù) GetMediaType() 獲取虽界,具體實(shí)現(xiàn)是在子類中汽烦,分別為:

視頻:AVMediaType.AVMEDIA_TYPE_VIDEO

音頻:AVMediaType.AVMEDIA_TYPE_AUDIO

  • 創(chuàng)建待解碼和解碼數(shù)據(jù)結(jié)構(gòu)
// base_decoder.cpp

void BaseDecoder::AllocFrameBuffer() {
    // 初始化待解碼和解碼數(shù)據(jù)結(jié)構(gòu)
    // 1)初始化AVPacket,存放解碼前的數(shù)據(jù)
    m_packet = av_packet_alloc();
    // 2)初始化AVFrame莉御,存放解碼后的數(shù)據(jù)
    m_frame = av_frame_alloc();
}

很簡單撇吞,通過兩個(gè)方法分配了內(nèi)存俗冻,供后面解碼的時(shí)候使用。

  • 解碼循環(huán)
// base_decoder.cpp

void BaseDecoder::LoopDecode() {
    if (STOP == m_state) { // 如果已被外部改變狀態(tài)牍颈,維持外部配置
        m_state = START;
    }

    LOG_INFO(TAG, LogSpec(), "Start loop decode")
    while(1) {
        if (m_state != DECODING &&
            m_state != START &&
            m_state != STOP) {
            Wait();
            // 恢復(fù)同步起始時(shí)間迄薄,去除等待流失的時(shí)間
            m_started_t = GetCurMsTime() - m_cur_t_s;
        }

        if (m_state == STOP) {
            break;
        }

        if (-1 == m_started_t) {
            m_started_t = GetCurMsTime();
        }

        if (DecodeOneFrame() != NULL) {
            SyncRender();
            Render(m_frame);

            if (m_state == START) {
                m_state = PAUSE;
            }
        } else {
            LOG_INFO(TAG, LogSpec(), "m_state = %d" ,m_state)
            if (ForSynthesizer()) {
                m_state = STOP;
            } else {
                m_state = FINISH;
            }
        }
    }
}

可以看到,這里進(jìn)入 while 死循環(huán)煮岁,其中融合了部分時(shí)間同步的代碼噪奄,同步的邏輯在之前硬解的文章有詳細(xì)的說明,具體參考 音視頻同步人乓。

不再細(xì)說勤篮,這里只看其中最重要的一個(gè)方法:DecodeOneFrame()

  • 解碼一幀數(shù)據(jù)

看具體代碼之前色罚,來看看 FFmpeg 是如何實(shí)現(xiàn)解碼的碰缔,分別是三個(gè)方法:

++av_read_frame(m_format_ctx, m_packet)++

m_format_ctx 中讀取一幀解封好的待解碼數(shù)據(jù),存放在 m_packet 中戳护;

++avcodec_send_packet(m_codec_ctx, m_packet)++

m_packet 發(fā)送到解碼器中解碼金抡,解碼好的數(shù)據(jù)存放在 m_codec_ctx 中;

++avcodec_receive_frame(m_codec_ctx, m_frame)++

接收一幀解碼好的數(shù)據(jù)腌且,存放在 m_frame 中梗肝。

// base_decoder.cpp

AVFrame* BaseDecoder::DecodeOneFrame() {
    int ret = av_read_frame(m_format_ctx, m_packet);
    while (ret == 0) {
        if (m_packet->stream_index == m_stream_index) {
            switch (avcodec_send_packet(m_codec_ctx, m_packet)) {
                case AVERROR_EOF: {
                    av_packet_unref(m_packet);
                    LOG_ERROR(TAG, LogSpec(), "Decode error: %s", av_err2str(AVERROR_EOF));
                    return NULL; //解碼結(jié)束
                }
                case AVERROR(EAGAIN):
                    LOG_ERROR(TAG, LogSpec(), "Decode error: %s", av_err2str(AVERROR(EAGAIN)));
                    break;
                case AVERROR(EINVAL):
                    LOG_ERROR(TAG, LogSpec(), "Decode error: %s", av_err2str(AVERROR(EINVAL)));
                    break;
                case AVERROR(ENOMEM):
                    LOG_ERROR(TAG, LogSpec(), "Decode error: %s", av_err2str(AVERROR(ENOMEM)));
                    break;
                default:
                    break;
            }
            int result = avcodec_receive_frame(m_codec_ctx, m_frame);
            if (result == 0) {
                ObtainTimeStamp();
                av_packet_unref(m_packet);
                return m_frame;
            } else {
                LOG_INFO(TAG, LogSpec(), "Receive frame error result: %d", av_err2str(AVERROR(result)))
            }
        }
        // 釋放packet
        av_packet_unref(m_packet);
        ret = av_read_frame(m_format_ctx, m_packet);
    }
    av_packet_unref(m_packet);
    LOGI(TAG, "ret = %d", ret)
    return NULL;
}

知道了解碼過程,其他的其實(shí)就是處理異常的情況铺董,比如:

  • 解碼需要等待時(shí)巫击,則重新將數(shù)據(jù)發(fā)送到解碼器,然后再取數(shù)據(jù)精续;

  • 解碼發(fā)生異常坝锰,讀取下一幀數(shù)據(jù),然后繼續(xù)解碼重付;

  • 如果解碼完成了顷级,返回空數(shù)據(jù) NULL


最后确垫,非常重要的是弓颈,解碼完一幀數(shù)據(jù)的時(shí)候,一定要調(diào)用 av_packet_unref(m_packet); 釋放內(nèi)存删掀,否則會導(dǎo)致內(nèi)存泄漏翔冀。

  • 解碼完畢,釋放資源

解碼完畢后爬迟,需要釋放所有 FFmpeg 相關(guān)的資源橘蜜,關(guān)閉解碼器菊匿。

還有一點(diǎn)要注意的是付呕,在初始化的時(shí)候计福,將 jstring 轉(zhuǎn)換得到的文件路徑也要釋放,并且要刪除全局引用徽职。

// base_deocder.cpp

void BaseDecoder::DoneDecode(JNIEnv *env) {
    LOG_INFO(TAG, LogSpec(), "Decode done and decoder release")
    // 釋放緩存
    if (m_packet != NULL) {
        av_packet_free(&m_packet);
    }
    if (m_frame != NULL) {
        av_frame_free(&m_frame);
    }
    // 關(guān)閉解碼器
    if (m_codec_ctx != NULL) {
        avcodec_close(m_codec_ctx);
        avcodec_free_context(&m_codec_ctx);
    }
    // 關(guān)閉輸入流
    if (m_format_ctx != NULL) {
        avformat_close_input(&m_format_ctx);
        avformat_free_context(m_format_ctx);
    }
    // 釋放轉(zhuǎn)換參數(shù)
    if (m_path_ref != NULL && m_path != NULL) {
        env->ReleaseStringUTFChars((jstring) m_path_ref, m_path);
        env->DeleteGlobalRef(m_path_ref);
    }

    // 通知子類釋放資源
    Release();
}

以上象颖,將解碼器的基礎(chǔ)結(jié)構(gòu)封裝好,只要繼承并實(shí)現(xiàn)規(guī)定的虛函數(shù)姆钉,即可實(shí)現(xiàn)視頻的解碼了说订。

四、視頻播放

視頻解碼器

這里有兩個(gè)重要的地方需要說明:

1. 視頻數(shù)據(jù)轉(zhuǎn)碼

我們知道潮瓶,視頻解碼出來以后陶冷,數(shù)據(jù)格式是 YUV ,而屏幕顯示的時(shí)候需要 RGBA毯辅,因此視頻解碼器中埂伦,需要對數(shù)據(jù)做一層轉(zhuǎn)換。

使用的是 FFmpeg 中的 SwsContext 工具思恐,轉(zhuǎn)換方法為 sws_scale沾谜,他們都隸屬于 swresampel 工具包。

sws_scale 既可以實(shí)現(xiàn)數(shù)據(jù)格式的轉(zhuǎn)化胀莹,同時(shí)可以對畫面寬高進(jìn)行縮放基跑。

2. 聲明渲染器

經(jīng)過轉(zhuǎn)換,視頻幀數(shù)據(jù)變成 RGBA 描焰,就可以渲染到手機(jī)屏幕上了媳否,這里有兩種方法:

  • 一是,通過本地窗口逆日,直接渲染數(shù)據(jù)漾月,這種方式無法實(shí)現(xiàn)對畫面的重新編輯
  • 二是飒责,通過 OpenGL ES 渲染,可實(shí)現(xiàn)對畫面的編輯

本文使用的是前者珊搀,OpenGL ES 渲染的方式將在后面的文章單獨(dú)講解。

新建目錄 src/main/cpp/decoder/video,并新建視頻解碼器 v_decoder疾捍。

看頭文件 v_decoder.h

// base_decoder.cpp

#ifndef LEARNVIDEO_V_DECODER_H
#define LEARNVIDEO_V_DECODER_H

#include "../base_decoder.h"
#include "../../render/video/video_render.h"
#include <jni.h>
#include <android/native_window_jni.h>
#include <android/native_window.h>

extern "C" {
#include <libavutil/imgutils.h>
#include <libswscale/swscale.h>
};

class VideoDecoder : public BaseDecoder {
private:
    const char *TAG = "VideoDecoder";

    //視頻數(shù)據(jù)目標(biāo)格式
    const AVPixelFormat DST_FORMAT = AV_PIX_FMT_RGBA;

    //存放YUV轉(zhuǎn)換為RGB后的數(shù)據(jù)
    AVFrame *m_rgb_frame = NULL;

    uint8_t *m_buf_for_rgb_frame = NULL;

    //視頻格式轉(zhuǎn)換器
    SwsContext *m_sws_ctx = NULL;

    //視頻渲染器
    VideoRender *m_video_render = NULL;

    //顯示的目標(biāo)寬
    int m_dst_w;
    //顯示的目標(biāo)高
    int m_dst_h;

    /**
     * 初始化渲染器
     */
    void InitRender(JNIEnv *env);

    /**
     * 初始化顯示器
     * @param env
     */
    void InitBuffer();

    /**
     * 初始化視頻數(shù)據(jù)轉(zhuǎn)換器
     */
    void InitSws();

public:
    VideoDecoder(JNIEnv *env, jstring path, bool for_synthesizer = false);
    ~VideoDecoder();
    void SetRender(VideoRender *render);

protected:
    AVMediaType GetMediaType() override {
        return AVMEDIA_TYPE_VIDEO;
    }

    /**
     * 是否需要循環(huán)解碼
     */
    bool NeedLoopDecode() override;

    /**
     * 準(zhǔn)備解碼環(huán)境
     * 注:在解碼線程中回調(diào)
     * @param env 解碼線程綁定的jni環(huán)境
     */
    void Prepare(JNIEnv *env) override;

    /**
     * 渲染
     * 注:在解碼線程中回調(diào)
     * @param frame 解碼RGBA數(shù)據(jù)
     */
    void Render(AVFrame *frame) override;

    /**
     * 釋放回調(diào)
     */
    void Release() override;

    const char *const LogSpec() override {
        return "VIDEO";
    };
};

#endif //LEARNVIDEO_V_DECODER_H

接下來看 v_deocder.cpp 實(shí)現(xiàn)奈辰,先看初始化相關(guān)的代碼:

// v_deocder.cpp

VideoDecoder::VideoDecoder(JNIEnv *env, jstring path, bool for_synthesizer)
: BaseDecoder(env, path, for_synthesizer) {
}

void VideoDecoder::Prepare(JNIEnv *env) {
    InitRender(env);
    InitBuffer();
    InitSws();
}

構(gòu)造函數(shù)很簡單,把相關(guān)的參數(shù)傳遞給父類 base_decoder 即可拾氓。

接下來是 Prepare 方法冯挎,這個(gè)方法是父類 base_decoder 中規(guī)定的子類必須實(shí)現(xiàn)的方法底哥,在初始化完解碼器之后調(diào)用咙鞍,回顧一下:

// base_decoder.cpp

void BaseDecoder::Decode(std::shared_ptr<BaseDecoder> that) {

    // 省略無關(guān)代碼...

    that->InitFFMpegDecoder(env);
    that->AllocFrameBuffer();

    //子類初始化方法調(diào)用
    that->Prepare(env); 

    that->LoopDecode();
    that->DoneDecode(env);

    // 省略無關(guān)代碼...

}

Prepare 中,初始化渲染器 InitRender 的先略過趾徽,后面詳細(xì)再講续滋。

看看數(shù)據(jù)格式轉(zhuǎn)化相關(guān)的初始化。

  • 存放數(shù)據(jù)緩存初始化:
// base_decoder.cpp

void VideoDecoder::InitBuffer() {
    m_rgb_frame = av_frame_alloc();
    // 獲取緩存大小
    int numBytes = av_image_get_buffer_size(DST_FORMAT, m_dst_w, m_dst_h, 1);
    // 分配內(nèi)存
    m_buf_for_rgb_frame = (uint8_t *) av_malloc(numBytes * sizeof(uint8_t));
    // 將內(nèi)存分配給RgbFrame孵奶,并將內(nèi)存格式化為三個(gè)通道后疲酌,分別保存其地址
    av_image_fill_arrays(m_rgb_frame->data, m_rgb_frame->linesize,
                         m_buf_for_rgb_frame, DST_FORMAT, m_dst_w, m_dst_h, 1);
}

通過 av_frame_alloc 方法初始化一塊 AVFrame ,注意該方法沒有分配緩存內(nèi)存了袁;

然后通過 av_image_get_buffer_size 方法計(jì)算所需內(nèi)存塊大小朗恳,其中

AVPixelFormat DST_FORMAT = AV_PIX_FMT_RGBA

m_dst_w: 為目標(biāo)畫面寬度(即畫面顯示時(shí)的實(shí)際寬度,將通過后續(xù)渲染器中具體的窗戶大小計(jì)算得出)
m_dst_h:為目標(biāo)畫面高度(即畫面顯示時(shí)的實(shí)際高度载绿,將通過后續(xù)渲染器中具體的窗戶大小計(jì)算得出)

接著通過 av_malloc 真正分配一塊內(nèi)存粥诫;

最后,通過 av_image_fill_arrays 將得到的這塊內(nèi)存給到 AVFrame崭庸,至此怀浆,內(nèi)存分配完成。

  • 數(shù)據(jù)轉(zhuǎn)換工具初始化
// base_decoder.cpp

void VideoDecoder::InitSws() {
    // 初始化格式轉(zhuǎn)換工具
    m_sws_ctx = sws_getContext(width(), height(), video_pixel_format(),
                               m_dst_w, m_dst_h, DST_FORMAT,
                               SWS_FAST_BILINEAR, NULL, NULL, NULL);
}

這個(gè)很簡單怕享,只要將原畫面數(shù)據(jù)和目標(biāo)畫面數(shù)據(jù)的長寬执赡、格式等傳遞進(jìn)去即可。

  • 釋放相關(guān)資源

在解碼完畢以后函筋,父類會調(diào)用子類 Release 方法沙合,以釋放子類中相關(guān)的資源。

// v_deocder.cpp

void VideoDecoder::Release() {
    LOGE(TAG, "[VIDEO] release")
    if (m_rgb_frame != NULL) {
        av_frame_free(&m_rgb_frame);
        m_rgb_frame = NULL;
    }
    if (m_buf_for_rgb_frame != NULL) {
        free(m_buf_for_rgb_frame);
        m_buf_for_rgb_frame = NULL;
    }
    if (m_sws_ctx != NULL) {
        sws_freeContext(m_sws_ctx);
        m_sws_ctx = NULL;
    }
    if (m_video_render != NULL) {
        m_video_render->ReleaseRender();
        m_video_render = NULL;
    }
}

初始化和資源釋放已經(jīng)完成跌帐,就剩下最后的渲染器配置了灌诅。

渲染器

剛剛上面說過,一般有兩種方式渲染畫面含末,那么就先把渲染器先定義好猜拾,方便后面擴(kuò)展。

定義視頻渲染器

新建目錄 src/main/cpp/media/render/video佣盒,并創(chuàng)建頭文件 video_render.h挎袜。

#ifndef LEARNVIDEO_VIDEORENDER_H
#define LEARNVIDEO_VIDEORENDER_H

#include <stdint.h>
#include <jni.h>

#include "../../one_frame.h"

class VideoRender {
public:
    virtual void InitRender(JNIEnv *env, int video_width, int video_height, int *dst_size) = 0;
    virtual void Render(OneFrame *one_frame) = 0;
    virtual void ReleaseRender() = 0;
};

#endif //LEARNVIDEO_VIDEORENDER_H

該類同樣是純虛類,類似 Javainterface

這里只是規(guī)定了幾個(gè)接口盯仪,分別是初始化紊搪、渲染、釋放資源全景。

實(shí)現(xiàn)本地窗口渲染器

新建目錄 src/main/cpp/media/render/video/native_render耀石,并創(chuàng)建頭文件 native_render 類。

native_render 頭文件:

// native_render.h

#ifndef LEARNVIDEO_NATIVE_RENDER_H
#define LEARNVIDEO_NATIVE_RENDER_H

#include <android/native_window.h>
#include <android/native_window_jni.h>
#include <jni.h>

#include "../video_render.h"
#include "../../../../utils/logger.h"

extern "C" {
#include <libavutil/mem.h>
};

class NativeRender: public VideoRender {
private:
    const char *TAG = "NativeRender";

    // Surface引用爸黄,必須使用引用滞伟,否則無法在線程中操作
    jobject m_surface_ref = NULL;

    // 存放輸出到屏幕的緩存數(shù)據(jù)
    ANativeWindow_Buffer m_out_buffer;

    // 本地窗口
    ANativeWindow *m_native_window = NULL;

    //顯示的目標(biāo)寬
    int m_dst_w;

    //顯示的目標(biāo)高
    int m_dst_h;

public:
    NativeRender(JNIEnv *env, jobject surface);
    ~NativeRender();
    void InitRender(JNIEnv *env, int video_width, int video_height, int *dst_size) override ;
    void Render(OneFrame *one_frame) override ;
    void ReleaseRender() override ;
};

可以看到,渲染器中持有一個(gè) Surface 引用炕贵,這就是我們非常熟悉的東西梆奈,前面一系列文章中,畫面渲染都是使用了它称开。

另外還有一個(gè)就是本地窗口 ANativeWindow 亩钟,只要Surface 綁定給 ANativeWindow,就可以通過本地窗口實(shí)現(xiàn) Surface 渲染了鳖轰。

看看渲染器的實(shí)現(xiàn) native_render.cpp 清酥。

  • 初始化
// native_render.cpp

ativeRender::NativeRender(JNIEnv *env, jobject surface) {
    m_surface_ref = env->NewGlobalRef(surface);
}

NativeRender::~NativeRender() {

}

void NativeRender::InitRender(JNIEnv *env, int video_width, int video_height, int *dst_size) {
    // 初始化窗口
    m_native_window = ANativeWindow_fromSurface(env, m_surface_ref);

    // 繪制區(qū)域的寬高
    int windowWidth = ANativeWindow_getWidth(m_native_window);
    int windowHeight = ANativeWindow_getHeight(m_native_window);

    // 計(jì)算目標(biāo)視頻的寬高
    m_dst_w = windowWidth;
    m_dst_h = m_dst_w * video_height / video_width;
    if (m_dst_h > windowHeight) {
        m_dst_h = windowHeight;
        m_dst_w = windowHeight * video_width / video_height;
    }
    LOGE(TAG, "windowW: %d, windowH: %d, dstVideoW: %d, dstVideoH: %d",
         windowWidth, windowHeight, m_dst_w, m_dst_h)

    //設(shè)置寬高限制緩沖區(qū)中的像素?cái)?shù)量
    ANativeWindow_setBuffersGeometry(m_native_window, windowWidth,
            windowHeight, WINDOW_FORMAT_RGBA_8888);

    dst_size[0] = m_dst_w;
    dst_size[1] = m_dst_h;
}

重點(diǎn)來看 InitRender 方法:

通過 ANativeWindow_fromSurfaceSurface 綁定給本地窗口;

通過 ANativeWindow_getWidth ANativeWindow_getHeight 可以獲取到 Surface 可顯示區(qū)域的寬高蕴侣;

然后焰轻,根據(jù)原始視頻畫面的寬高 video_width video_height 以及可現(xiàn)實(shí)區(qū)域的寬高,進(jìn)行畫面縮放睛蛛,可以計(jì)算出最終顯示的畫面的寬高鹦马,并賦值給解碼器。

視頻解碼器 v_decoder 在獲取到目標(biāo)畫面寬高之后忆肾,就可以去初始化數(shù)據(jù)轉(zhuǎn)化緩存區(qū)的大小了荸频。

最后,通過 ANativeWindow_setBuffersGeometry 設(shè)置一下本地窗口緩存區(qū)大小客冈,完成初始化旭从。

  • 渲染

兩個(gè)重要的本地方法:

ANativeWindow_lock 鎖定窗口,并獲取到輸出緩沖區(qū) m_out_buffer场仲。

ANativeWindow_unlockAndPost 釋放窗口锯梁,并將緩沖數(shù)據(jù)繪制到屏幕上毒坛。

// native_render.cpp

void NativeRender::Render(OneFrame *one_frame) {
    //鎖定窗口
    ANativeWindow_lock(m_native_window, &m_out_buffer, NULL);
    uint8_t *dst = (uint8_t *) m_out_buffer.bits;
    // 獲取stride:一行可以保存的內(nèi)存像素?cái)?shù)量*4(即:rgba的位數(shù))
    int dstStride = m_out_buffer.stride * 4;
    int srcStride = one_frame->line_size;

    // 由于window的stride和幀的stride不同,因此需要逐行復(fù)制
    for (int h = 0; h < m_dst_h; h++) {
        memcpy(dst + h * dstStride, one_frame->data + h * srcStride, srcStride);
    }
    //釋放窗口
    ANativeWindow_unlockAndPost(m_native_window);
}

渲染過程看起來很復(fù)雜,主要是因?yàn)檫@里有一個(gè) stride 的概念保檐,指的是一幀畫面每一行數(shù)據(jù)的寬度大小月劈。

比如這里的數(shù)據(jù)格式是 RGBA 僧界,一行畫面的像素是 8 個(gè),那么總共的 stride 寬度就是 8*4 = 32 棒坏。
為什么需要轉(zhuǎn)換呢?原因是本地窗口的 stride 大小可能和視頻畫面數(shù)據(jù)的 stride 不一致遭笋,直接將視頻畫面數(shù)據(jù)給到本地窗口時(shí)坝冕,可能會導(dǎo)致數(shù)據(jù)讀取不一致,最終導(dǎo)致花屏瓦呼。

所以喂窟,這里需要根據(jù)本地窗口的 dstStride 和視頻畫面數(shù)據(jù)的 srcStride,將數(shù)據(jù)一行一行復(fù)制(memcpy)央串。

渲染器調(diào)用

最后來看下磨澡,視頻解碼器 v_decoder 中對渲染器的調(diào)用

// v_decoder.cpp

void VideoDecoder::SetRender(VideoRender *render) {
    this->m_video_render = render;
}

void VideoDecoder::InitRender(JNIEnv *env) {
    if (m_video_render != NULL) {
        int dst_size[2] = {-1, -1};
        m_video_render->InitRender(env, width(), height(), dst_size);

        m_dst_w = dst_size[0];
        m_dst_h = dst_size[1];
        if (m_dst_w == -1) {
            m_dst_w = width();
        }
        if (m_dst_h == -1) {
            m_dst_w = height();
        }
        LOGI(TAG, "dst %d, %d", m_dst_w, m_dst_h)
    } else {
        LOGE(TAG, "Init render error, you should call SetRender first!")
    }
}

void VideoDecoder::Render(AVFrame *frame) {
    sws_scale(m_sws_ctx, frame->data, frame->linesize, 0,
              height(), m_rgb_frame->data, m_rgb_frame->linesize);
    OneFrame * one_frame = new OneFrame(m_rgb_frame->data[0], m_rgb_frame->linesize[0], frame->pts, time_base(), NULL, false);
    m_video_render->Render(one_frame);
}

一是,將渲染設(shè)置給視頻解碼器蹋辅;

二是钱贯,調(diào)用渲染器的 InitRender 方法初始化渲染器挫掏,并獲得目標(biāo)畫面寬高

最后是侦另,調(diào)用渲染器 Render 方法,進(jìn)行渲染尉共。

其中褒傅,OneFrame 是自定義類,用來封裝一幀數(shù)據(jù)相關(guān)的內(nèi)容袄友,知道即可殿托,具體可以查看【工程源碼】。

編寫播放器

以上剧蚣,完成了 :

  1. 基礎(chǔ)解碼器 的封裝 --> 視頻解碼器 的實(shí)現(xiàn);
  2. 渲染器的定義 --> 本地渲染窗口 的實(shí)現(xiàn)支竹。

最后就差把他們整合在一起,實(shí)現(xiàn)播放了鸠按。

src/main/cpp/media 目錄下新建一個(gè)播放器 player礼搁,如下:

// player.h

#ifndef LEARNINGVIDEO_PLAYER_H
#define LEARNINGVIDEO_PLAYER_H

#include "decoder/video/v_decoder.h"

class Player {
private:
    VideoDecoder *m_v_decoder;
    VideoRender *m_v_render;

public:
    Player(JNIEnv *jniEnv, jstring path, jobject surface);
    ~Player();

    void play();
    void pause();
};

#endif //LEARNINGVIDEO_PLAYER_H

播放器持有一個(gè)視頻解碼器和一個(gè)視頻渲染器,以及一個(gè)播放和暫停方法目尖。

// player.cpp

#include "player.h"
#include "render/video/native_render/native_render.h"

Player::Player(JNIEnv *jniEnv, jstring path, jobject surface) {
    m_v_decoder = new VideoDecoder(jniEnv, path);
    m_v_render = new NativeRender(jniEnv, surface);
    m_v_decoder->SetRender(m_v_render);
}

Player::~Player() {
    // 此處不需要 delete 成員指針
    // 在BaseDecoder中的線程已經(jīng)使用智能指針馒吴,會自動釋放
}

void Player::play() {
    if (m_v_decoder != NULL) {
        m_v_decoder->GoOn();
    }
}

void Player::pause() {
    if (m_v_decoder != NULL) {
        m_v_decoder->Pause();
    }
}

代碼很簡單,就是把解碼器和渲染器關(guān)聯(lián)起來瑟曲。

將源代碼加入編譯

雖然上面完成了各個(gè)功能模塊的編寫饮戳,但是編譯器不會自動把它們加入編譯。要想讓 C++ 代碼加入編譯洞拨,需要手動在 CMakeLists.txt 文件中配置扯罐,配置的位置和默認(rèn)的 native-lib.cpp 相同,羅列在后面即可烦衣。

# CMakeLists.txt

// 省略無關(guān)配置
//......

# 配置目標(biāo)so庫編譯信息
add_library( # Sets the name of the library.
        native-lib

        # Sets the library as a shared library.
        SHARED

        # Provides a relative path to your source file(s).
        native-lib.cpp

        # 工具
        ${CMAKE_SOURCE_DIR}/utils/logger.h
        ${CMAKE_SOURCE_DIR}/utils/timer.c

        # 播放器
        ${CMAKE_SOURCE_DIR}/media//player.cpp

        # 解碼器
        ${CMAKE_SOURCE_DIR}/media//one_frame.h
        ${CMAKE_SOURCE_DIR}/media/decoder/i_decoder.h
        ${CMAKE_SOURCE_DIR}/media/decoder/decode_state.h
        ${CMAKE_SOURCE_DIR}/media/decoder/base_decoder.cpp
        ${CMAKE_SOURCE_DIR}/media/decoder/video/v_decoder.cpp

        # 渲染器
        ${CMAKE_SOURCE_DIR}/media/render/video/video_render.h
        ${CMAKE_SOURCE_DIR}/media/render/video/native_render/native_render.cpp
        )

// 省略無關(guān)配置
//......

如果類只有 .h 頭文件的話歹河,就只寫 .h 文件齿椅,如果類既有頭文件,又有 .cpp 實(shí)現(xiàn)文件启泣,則只需要配置 .cpp 文件

需要注意的是:在創(chuàng)建好每個(gè)類的時(shí)候涣脚,就需要將其配置到 CMakeLists.txt 中,否則在編寫代碼的時(shí)寥茫,可能無法導(dǎo)入相關(guān)的庫頭文件遣蚀,也就沒法通過編譯。

編寫 JNI 接口

接下來就需要將播放器暴露給 Java 層使用了纱耻,這時(shí)候就需要用到 JNI 的接口文件 native-lib.cpp 了芭梯。

開始編寫 JNI 接口之前,先在 FFmpegActivity 中寫好相應(yīng)的接口:

// FFmpegActivity.kt

class FFmpegActivity: AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_ffmpeg_info)
        tv.text = ffmpegInfo()
        initSfv()
    }

    private fun initSfv() {
        sfv.holder.addCallback(object: SurfaceHolder.Callback {
            override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) {

            }

            override fun surfaceDestroyed(holder: SurfaceHolder) {
            }

            override fun surfaceCreated(holder: SurfaceHolder) {
                if (player == null) {
                    player = createPlayer(path, holder.surface)
                    play(player!!)
                }
            }
        })
    }

//------------ JNI 相關(guān)接口方法 ----------------------

    private external fun ffmpegInfo(): String

    private external fun createPlayer(path: String, surface: Surface): Int

    private external fun play(player: Int)

    private external fun pause(player: Int)

    companion object {
        init {
            System.loadLibrary("native-lib")
        }
    }
}

接口很簡單:

createPlayer(path: String, surface: Surface): Int: 創(chuàng)建播放器弄喘,并返回播放器對象地址

play(player: Int):播放玖喘,參數(shù)為播放器對象

pause(player: Int): 暫停,參數(shù)為播放器對象

播放器的創(chuàng)建時(shí)機(jī)為 SurfaceView 初始化完成時(shí): surfaceCreated蘑志。

頁面布局 xml 如下:

<android.support.constraint.ConstraintLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent" android:layout_height="match_parent">
    <ScrollView
            android:layout_width="match_parent"
            android:layout_height="match_parent">
        <LinearLayout
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:orientation="vertical">
            <SurfaceView android:id="@+id/sfv"
                    android:layout_width="match_parent"
                    android:layout_height="200dp" />
            <TextView android:id="@+id/tv"
                    android:layout_width="match_parent"
                    android:layout_height="match_parent"/>
        </LinearLayout>
    </ScrollView>
</android.support.constraint.ConstraintLayout>

接下來累奈,就根據(jù)以上三個(gè)接口,在 JNI 中編寫對應(yīng)的接口急但。


// native-lib.cpp

#include <jni.h>
#include <string>
#include <unistd.h>
#include "media/player.h"

extern "C" {

    JNIEXPORT jint JNICALL
    Java_com_cxp_learningvideo_FFmpegActivity_createPlayer(JNIEnv *env,
            jobject  /* this */,
            jstring path,
            jobject surface) {
            Player *player = new Player(env, path, surface);
            return (jint) player;
        }

    JNIEXPORT void JNICALL
    Java_com_cxp_learningvideo_FFmpegActivity_play(JNIEnv *env,
                                                   jobject  /* this */,
                                                   jint player) {
        Player *p = (Player *) player;
        p->play();
    }

    JNIEXPORT void JNICALL
    Java_com_cxp_learningvideo_FFmpegActivity_pause(JNIEnv *env,
                                                   jobject  /* this */,
                                                   jint player) {
        Player *p = (Player *) player;
        p->pause();
    }
}

很簡單澎媒,相信大家都看得懂,其實(shí)就是初始化一個(gè)播放器對象指針波桩,然后返回給 Java 層保存戒努,后面的播放和暫停操作都是 Java 層將這個(gè)播放器指針再傳給 JNI 層做具體操作。

image

五镐躲、總結(jié)

簡單做一下總結(jié):

  • 初始化:根據(jù) FFmpeg 提供的一些功能接口储玫,對解碼器做初始化

    • 輸入文件碼流上下文 AVFormatContext
    • 解碼器上下文 AVCodecContext
    • 解碼器 AVCodec
    • 分配數(shù)據(jù)緩存空間 AVPacket(存放待解碼數(shù)據(jù)) 和 AVFrame (存放已解碼數(shù)據(jù))
  • 解碼:通過 FFmpeg 提供的解碼接口進(jìn)行解碼

    • av_read_frame 讀取待解碼數(shù)據(jù)到 AVPacket
    • avcodec_send_packet 發(fā)送 AVPacket 到解碼器解碼
    • avcodec_receive_frame 讀取解碼好的數(shù)據(jù)到 AVFrame
  • 轉(zhuǎn)碼和縮放:通過 FFmpeg 提供的轉(zhuǎn)碼接口將 YUV 轉(zhuǎn)換為 RGBA

    • sws_getContext 初始化轉(zhuǎn)化工具 SwsContext
    • sws_scale 執(zhí)行數(shù)據(jù)轉(zhuǎn)換
  • 渲染:通過 Android 提供的接口將視頻數(shù)據(jù)渲染到屏幕上

    • ANativeWindow_fromSurface 綁定 Surface 到本地窗口
    • ANativeWindow_getWidth/ANativeWindow_getWidth 獲取 Surface 寬高
    • ANativeWindow_setBuffersGeometry 設(shè)置屏幕緩沖區(qū)大小
    • ANativeWindow_lock 鎖定窗口,獲取顯示緩沖區(qū)
    • 根據(jù) Stride 將數(shù)據(jù)復(fù)制(memcpy)到緩沖區(qū)
    • ANativeWindow_unlockAndPost 解鎖窗口萤皂,并顯示

轉(zhuǎn)發(fā)自:http://www.reibang.com/p/d7c8f49d9ea4

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末撒穷,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子敌蚜,更是在濱河造成了極大的恐慌桥滨,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,591評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件弛车,死亡現(xiàn)場離奇詭異齐媒,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)纷跛,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,448評論 3 392
  • 文/潘曉璐 我一進(jìn)店門喻括,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人贫奠,你說我怎么就攤上這事唬血⊥” “怎么了?”我有些...
    開封第一講書人閱讀 162,823評論 0 353
  • 文/不壞的土叔 我叫張陵拷恨,是天一觀的道長脖律。 經(jīng)常有香客問我,道長腕侄,這世上最難降的妖魔是什么小泉? 我笑而不...
    開封第一講書人閱讀 58,204評論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮冕杠,結(jié)果婚禮上微姊,老公的妹妹穿的比我還像新娘。我一直安慰自己分预,他們只是感情好兢交,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,228評論 6 388
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著笼痹,像睡著了一般与倡。 火紅的嫁衣襯著肌膚如雪昆稿。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,190評論 1 299
  • 那天净响,我揣著相機(jī)與錄音,去河邊找鬼馋贤。 笑死,一個(gè)胖子當(dāng)著我的面吹牛犹芹,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播屿笼,決...
    沈念sama閱讀 40,078評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼挑辆!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起魁亦,我...
    開封第一講書人閱讀 38,923評論 0 274
  • 序言:老撾萬榮一對情侶失蹤绞灼,失蹤者是張志新(化名)和其女友劉穎印叁,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體跃洛,經(jīng)...
    沈念sama閱讀 45,334評論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡穴张,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,550評論 2 333
  • 正文 我和宋清朗相戀三年找颓,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了佛析。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,727評論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出棒拂,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 35,428評論 5 343
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏狮鸭。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,022評論 3 326
  • 文/蒙蒙 一从藤、第九天 我趴在偏房一處隱蔽的房頂上張望懊蒸。 院中可真熱鬧骑丸,春花似錦铸豁、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,672評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至靡馁,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背结缚。 一陣腳步聲響...
    開封第一講書人閱讀 32,826評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人锌奴。 一個(gè)月前我還...
    沈念sama閱讀 47,734評論 2 368
  • 正文 我出身青樓服球,卻偏偏與公主長得像往枣,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子雕沉,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,619評論 2 354