首先,這一系列文章均基於本身的理解和實踐,可能有不對的地方,歡迎你們指正。
其次,這是一個入門系列,涉及的知識也僅限於夠用,深刻的知識網上也有許許多多的博文供你們學習了。
最後,寫文章過程當中,會借鑑參考其餘人分享的文章,會在文章最後列出,感謝這些做者的分享。android
碼字不易,轉載請註明出處!git
教程代碼:【Github傳送門】 |
---|
基於 FFmpeg 4.x 的音視頻解碼流程,重點講解如何實現視頻的播放。github
Hi~ 久等了!緩存
本文很長,由於可能有比較多的小夥伴對 JNI
C/C++
不是很熟悉,因此本文比較詳細的對 FFmpeg
用到的代碼進行講解,完整的演示了一遍 FFmpeg
的解碼和渲染過程,而且對解碼過程進行了封裝。bash
爲了方便講解和閱讀理解,代碼採起分塊的方式進行講解,也就是說,不會直接將整個類的內容完整的貼出來。數據結構
可是每部分代碼都會在開頭註明是屬於那個文件,哪一個類的。若是想要看完整的代碼,請直接查看 【Github 倉庫】。框架
本文須要 C/C++
基礎知識,對 C/C++
不熟悉的能夠查看本人的另外一篇文章: 【Android NDK入門:C++基礎知識】。jvm
請耐心地閱讀,相信看完後能夠對 FFmpeg
解碼有可觀的理解。ide
在 上一篇文章 中,把 FFmpeg
相關的庫都引入到 Android
工程中了,有如下幾個庫:函數
庫 | 介紹 |
---|---|
avcodec | 音視頻編解碼核心庫 |
avformat | 音視頻容器格式的封裝和解析 |
avutil | 核心工具庫 |
swscal | 圖像格式轉換的模塊 |
swresampel | 音頻重採樣 |
avfilter | 音視頻濾鏡庫 如視頻加水印、音頻變聲 |
avdevice | 輸入輸出設備庫,提供設備數據的輸入與輸出 |
FFmpeg 就是依靠以上幾個庫,實現了強大的音視頻編碼、解碼、編輯、轉換、採集等能力。
在前面的系列文章中,利用了 Android
提供的原生硬解碼能力,使用實現了視頻的解碼和播放。
總結起來有如下的流程:
Mp4
文件中的編碼數據,並送入解碼器解碼
FFmpeg
解碼無非也就是以上過程,只不過FFmpeg
是利用CPU
的計算能力來解碼而已。
FFmpeg
初始化的流程相對 Android
原生硬解碼來講仍是比較瑣碎的,可是流程都是固定的,一旦封裝起來就能夠直接套用了。
首先來看一下初始化的流程圖
其實就是根據待解碼文件的格式,進行一系列參數的初始化。
其中,有幾個 結構體
比較重要,分別是 AVFormatContext
(format_ctx)、AVCodecContext
(codec_ctx)、AVCodec
(codec)
結構體 :FFmpeg 是基於
C
語言開發的,咱們知道C
語言是面向過程的語言,也就是說不像C++
有類來封裝內部數據。可是C
提供告終構體,能夠用來實現數據的封裝,達到相似於類的效果。
AVFormatContext:隸屬於 avformat
庫,存放這碼流數據的上下文,主要用於音視頻的 封裝
和 解封
。
AVCodecContext:隸屬於 avcodec
庫,存放編解碼器參數上下文,主要用於對音視頻數據進行 編碼
和 解碼
。
AVCodec:隸屬於 avcodec
庫,音視頻編解碼器,真正編解碼執行者。
一樣的,經過一個流程圖來講明具體解碼過程:
在初始化完 FFmpeg
後,就能夠進行具體的數據幀解碼了。
從上圖能夠看到,FFmpeg
首先將數據提取爲一個 AVPacket
(avpacket),而後經過解碼,將數據解碼爲一幀能夠渲染的數據,稱爲 AVFrame
(frame)。
一樣的,AVPacket
和 AVFrame
也是兩個結構體,裏面封裝了具體的數據。
有了以上對解碼流程的瞭解,就能夠根據上面的 流程圖
來編寫代碼了。
根據以往的經驗,既然 FFmepg
的初始化和解碼流程都是一些瑣碎重複的工做,那麼咱們必然是要對其進行封裝的,以便更好的複用和拓展。
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
複製代碼
這是一個枚舉,定義瞭解碼器解碼的狀態
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;
};
複製代碼
這是一個純虛類,相似 Java
的 interface
(具體可查看 Android NDK入門:C++ 基礎知識),定義瞭解碼器該有的基礎方法。
base_decoder
。在src/main/cpp/media/decoder
目錄上,右鍵 New
-> C++ Class
輸入 base_decoder
,該類用於封裝解碼中最基礎的流程。
會生成兩個文件:base_decoder.h
、base_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";
//-------------定義解碼相關------------------------------
// 解碼信息上下文
AVFormatContext *m_format_ctx = NULL;
// 解碼器
AVCodec *m_codec = NULL;
// 解碼器上下文
AVCodecContext *m_codec_ctx = NULL;
// 待解碼包
AVPacket *m_packet = NULL;
// 最終解碼數據
AVFrame *m_frame = NULL;
// 當前播放時間
int64_t m_cur_t_s = 0;
// 總時長
long m_duration = 0;
// 開始播放的時間
int64_t m_started_t = -1;
// 解碼狀態
DecodeState m_state = STOP;
// 數據流索引
int m_stream_index = -1;
// 省略其餘
// ......
}
複製代碼
注意:在引入
FFmpeg
相關庫的頭文件時,須要注意把#include
放到extern "C" {}
中。由於FFmpeg
是C
語言寫的,因此在引入到C++
文件中的時候,須要標記以C
的方式來編譯,不然會致使編譯出錯。
在頭文件中,先聲明在 cpp
須要用到的相關變量,重點就是上一節提到的幾個解碼相關的結構體。
//base_decoder.h
class BaseDecoder: public IDecoder {
private:
const char *TAG = "BaseDecoder";
//-------------定義解碼相關------------------------------
//省略....
//-----------------私有方法------------------------------
/** * 初始化FFMpeg相關的參數 * @param env jvm環境 */
void InitFFMpegDecoder(JNIEnv * env);
/** * 分配解碼過程當中須要的緩存 */
void AllocFrameBuffer();
/** * 循環解碼 */
void LoopDecode();
/** * 獲取當前幀時間戳 */
void ObtainTimeStamp();
/** * 解碼完成 * @param env jvm環境 */
void DoneDecode(JNIEnv *env);
/** * 時間同步 */
void SyncRender();
// 省略其餘
// ......
}
複製代碼
i_decoder
,還須要實現其中規定的通用方法。//base_decoder.h
class BaseDecoder: public IDecoder {
//省略其餘
//......
public:
//--------構造方法和析構方法-------------
BaseDecoder(JNIEnv *env, jstring path);
virtual ~BaseDecoder();
//--------實現基礎類方法-----------------
void GoOn() override;
void Pause() override;
void Stop() override;
bool IsRunning() override;
long GetDuration() override;
long GetCurPos() override;
}
複製代碼
咱們知道,解碼是一個很是耗時的操做,就像原生硬解同樣,咱們須要開啓一個線程來承載解碼任務。因此,先在頭文件中定義好線程相關的變量和方法。
//base_decoder.h
class BaseDecoder: public IDecoder {
private:
//省略其餘
//......
// -------------------定義線程相關-----------------------------
// 線程依附的JVM環境
JavaVM *m_jvm_for_thread = NULL;
// 原始路徑jstring引用,不然沒法在線程中操做
jobject m_path_ref = NULL;
// 通過轉換的路徑
const char *m_path = NULL;
// 線程等待鎖變量
pthread_mutex_t m_mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t m_cond = PTHREAD_COND_INITIALIZER;
/** * 新建解碼線程 */
void CreateDecodeThread();
/** * 靜態解碼方法,用於解碼線程回調 * @param that 當前解碼器 */
static void Decode(std::shared_ptr<BaseDecoder> that);
protected:
/** * 進入等待 */
void Wait(long second = 0);
/** * 恢復解碼 */
void SendSignal();
}
複製代碼
//base_decoder.h
class BaseDecoder: public IDecoder {
protected:
/** * 子類準備回調方法 * @note 注:在解碼線程中回調 * @param env 解碼線程綁定的JVM環境 */
virtual void Prepare(JNIEnv *env) = 0;
/** * 子類渲染回調方法 * @note 注:在解碼線程中回調 * @param frame 視頻:一幀YUV數據;音頻:一幀PCM數據 */
virtual void Render(AVFrame *frame) = 0;
/** * 子類釋放資源回調方法 */
virtual void Release() = 0;
}
複製代碼
以上,就定義好了解碼類的基礎結構:
FFmpeg
解碼相關的結構體參數在 base_decoder.cpp
中,實現頭文件中聲明的方法
// 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虛擬機,爲建立線程做準備
env->GetJavaVM(&m_jvm_for_thread);
}
void BaseDecoder::CreateDecodeThread() {
// 使用智能指針,線程結束時,自動刪除本類指針
std::shared_ptr<BaseDecoder> that(this);
std::thread t(Decode, that);
t.detach();
}
複製代碼
構造函數很簡單,傳入 JNI
環境變量,以及待解碼文件路徑。
在 Init
方法中,由於 jstring
並不是 C++
的標準類型,須要將 jstring
類型的 path
轉換爲 char
類型,才能使用。
說明:因爲
JNIEnv
和線程
是一一對應的,也就是說,在Android
中,JNI環境
是和線程綁定的,每個線程都有一個獨立的JNIEnv
環境,而且互相之間不可訪問。因此若是要在新的線程中訪問JNIEnv
,須要爲這個線程建立一個新的JNIEnv
。
在 Init
方法的最後,經過 env->GetJavaVM(&m_jvm_for_thread)
獲取到 JavaVM
實例,保存到 m_jvm_for_thread
,該實例是全部共享的 ,經過它就能夠爲解碼線程獲取一個新的 JNIEnv
環境。
在 C++
中建立線程很是簡單,只需兩句話,就能夠啓動一個線程:
std::thread t(靜態方法, 靜態方法參數);
t.detach();
複製代碼
也就是說,這個線程須要一個靜態方法做爲參數,啓動之後,會回調這個靜態方法,而且能夠給這個靜態方法傳遞參數。
另外,CreateDecodeThread
方法中的第一代碼,是用於建立一個智能指針。
咱們知道,
C++
new
出來的指針對象是須要咱們手動delete
刪除的,不然就會出現內存泄漏。而智能指針的做用就是幫咱們實現內存管理。
當這個指針的引用計數爲 0 時,就會自動銷燬。也就是說,不須要咱們本身去手動 delete
。
std::shared_ptr<BaseDecoder> that(this);
複製代碼
這裏將 this
封裝成名爲 that
的智能指針,那麼在外部使用解碼器的時候,就不須要手動釋放內存了,當解碼線程退出的時候,會自動銷燬,並調用析構函數。
// base_decoder.cpp
void BaseDecoder::Decode(std::shared_ptr<BaseDecoder> that) {
JNIEnv * env;
//將線程附加到虛擬機,並獲取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);
// 分配解碼幀數據內存
that->AllocFrameBuffer();
// 回調子類方法,通知子類解碼器初始化完畢
that->Prepare(env);
// 進入解碼循環
that->LoopDecode();
// 退出解碼
that->DoneDecode(env);
//解除線程和jvm關聯
that->m_jvm_for_thread->DetachCurrentThread();
}
複製代碼
在 base_decoder.h
頭文件聲明中, Decode
是一個靜態的成員方法。
首先爲解碼線程建立了 JNIEnv
,失敗則直接退出解碼。
以上 Decode
方法中就是分步調用對應的方法,很簡單,看註釋便可。
接下來看具體的分步調用的內容。
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 獲取解碼器參數
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")
}
複製代碼
看起來好像很複雜,實際上套路都是同樣的,一開始看會感到不適應,主要是由於這些方法是面向過程的調用方法,和平時使用的面嚮對象語言使用習慣不太同樣。
舉個例子:
上面代碼中,打開文件的方法是這樣的:
avformat_open_input(&m_format_ctx, m_path, NULL, NULL);
複製代碼
而若是是面向對象的話,代碼一般是這樣的:
// 注意:如下爲僞代碼,僅用於舉例說明
m_format_ctx.avformat_open_input(m_path);
複製代碼
那麼怎麼理解 C
中的這種面向過程的調用呢?
咱們知道 m_format_ctx
是結構體,封裝了具體的數據,那麼 avformat_open_input
這個方法其實就是操做這個結構體的方法,不一樣的方法調用,是對結構體中不一樣數據的操做。
具體流程請看上面的註釋,不在細說,其實就是第一節中 【初始化流程圖】 中步驟的實現。
有兩點須要注意的:
FFmpeg
中帶有 alloc
字樣的方法,一般只是初始化對應的結構體,可是具體的參數和數據緩存區,通常都要通過另外方法的初始化才能使用,好比 m_format_ctx
, m_codec_ctx
:
// 建立
m_format_ctx = avformat_alloc_context();
// 初始化流信息
avformat_open_input(&m_format_ctx, m_path, NULL, NULL)
-------------------------------------------------------
// 建立
m_codec_ctx = avcodec_alloc_context3(m_codec);
//初始化具體內容
avcodec_parameters_to_context(m_codec_ctx, codecPar);
複製代碼
咱們知道音視頻數據一般封裝在不一樣的軌道中,因此,要想獲取到正確的音視頻數據,就須要先獲取到對應的索引。
音視頻的數據類型,經過虛函數 GetMediaType()
獲取,具體實現是在子類中,分別爲:
視頻:AVMediaType.AVMEDIA_TYPE_VIDEO
音頻:AVMediaType.AVMEDIA_TYPE_AUDIO
// base_decoder.cpp
void BaseDecoder::AllocFrameBuffer() {
// 初始化待解碼和解碼數據結構
// 1)初始化AVPacket,存放解碼前的數據
m_packet = av_packet_alloc();
// 2)初始化AVFrame,存放解碼後的數據
m_frame = av_frame_alloc();
}
複製代碼
很簡單,經過兩個方法分配了內存,供後面解碼的時候使用。
// base_decoder.cpp
void BaseDecoder::LoopDecode() {
if (STOP == m_state) { // 若是已被外部改變狀態,維持外部配置
m_state = START;
}
LOG_INFO(TAG, LogSpec(), "Start loop decode")
while(1) {
if (m_state != DECODING &&
m_state != START &&
m_state != STOP) {
Wait();
// 恢復同步起始時間,去除等待流失的時間
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;
}
}
}
}
複製代碼
能夠看到,這裏進入 while
死循環,其中融合了部分時間同步的代碼,同步的邏輯在以前硬解的文章有詳細的說明,具體參考 音視頻同步。
再也不細說,這裏只看其中最重要的一個方法:DecodeOneFrame()
。
看具體代碼以前,來看看 FFmpeg
是如何實現解碼的,分別是三個方法:
++av_read_frame(m_format_ctx, m_packet)++:
從 m_format_ctx
中讀取一幀解封好的待解碼數據,存放在 m_packet
中;
++avcodec_send_packet(m_codec_ctx, m_packet)++:
將 m_packet
發送到解碼器中解碼,解碼好的數據存放在 m_codec_ctx
中;
++avcodec_receive_frame(m_codec_ctx, m_frame)++:
接收一幀解碼好的數據,存放在 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; //解碼結束
}
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;
}
複製代碼
知道了解碼過程,其餘的其實就是處理異常的狀況,好比:
解碼須要等待時,則從新將數據發送到解碼器,而後再取數據;
解碼發生異常,讀取下一幀數據,而後繼續解碼;
若是解碼完成了,返回空數據 NULL
;
最後,很是重要的是,解碼完一幀數據的時候,必定要調用 av_packet_unref(m_packet);
釋放內存,不然會致使內存泄漏。
解碼完畢後,須要釋放全部 FFmpeg
相關的資源,關閉解碼器。
還有一點要注意的是,在初始化的時候,將 jstring
轉換獲得的文件路徑也要釋放,而且要刪除全局引用。
// 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);
}
// 關閉解碼器
if (m_codec_ctx != NULL) {
avcodec_close(m_codec_ctx);
avcodec_free_context(&m_codec_ctx);
}
// 關閉輸入流
if (m_format_ctx != NULL) {
avformat_close_input(&m_format_ctx);
avformat_free_context(m_format_ctx);
}
// 釋放轉換參數
if (m_path_ref != NULL && m_path != NULL) {
env->ReleaseStringUTFChars((jstring) m_path_ref, m_path);
env->DeleteGlobalRef(m_path_ref);
}
// 通知子類釋放資源
Release();
}
複製代碼
以上,將解碼器的基礎結構封裝好,只要繼承並實現規定的虛函數,便可實現視頻的解碼了。
這裏有兩個重要的地方須要說明:
1. 視頻數據轉碼
咱們知道,視頻解碼出來之後,數據格式是 YUV
,而屏幕顯示的時候須要 RGBA
,所以視頻解碼器中,須要對數據作一層轉換。
使用的是 FFmpeg
中的 SwsContext
工具,轉換方法爲 sws_scale
,他們都隸屬於 swresampel
工具包。
sws_scale
既能夠實現數據格式的轉化,同時能夠對畫面寬高進行縮放。
2. 聲明渲染器
通過轉換,視頻幀數據變成 RGBA
,就能夠渲染到手機屏幕上了,這裏有兩種方法:
OpenGL ES
渲染,可實現對畫面的編輯本文使用的是前者,
OpenGL ES
渲染的方式將在後面的文章單獨講解。
新建目錄 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";
//視頻數據目標格式
const AVPixelFormat DST_FORMAT = AV_PIX_FMT_RGBA;
//存放YUV轉換爲RGB後的數據
AVFrame *m_rgb_frame = NULL;
uint8_t *m_buf_for_rgb_frame = NULL;
//視頻格式轉換器
SwsContext *m_sws_ctx = NULL;
//視頻渲染器
VideoRender *m_video_render = NULL;
//顯示的目標寬
int m_dst_w;
//顯示的目標高
int m_dst_h;
/** * 初始化渲染器 */
void InitRender(JNIEnv *env);
/** * 初始化顯示器 * @param env */
void InitBuffer();
/** * 初始化視頻數據轉換器 */
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;
}
/** * 是否須要循環解碼 */
bool NeedLoopDecode() override;
/** * 準備解碼環境 * 注:在解碼線程中回調 * @param env 解碼線程綁定的jni環境 */
void Prepare(JNIEnv *env) override;
/** * 渲染 * 注:在解碼線程中回調 * @param frame 解碼RGBA數據 */
void Render(AVFrame *frame) override;
/** * 釋放回調 */
void Release() override;
const char *const LogSpec() override {
return "VIDEO";
};
};
#endif //LEARNVIDEO_V_DECODER_H
複製代碼
接下來看 v_deocder.cpp
實現,先看初始化相關的代碼:
// 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();
}
複製代碼
構造函數很簡單,把相關的參數傳遞給父類 base_decoder
便可。
接下來是 Prepare
方法,這個方法是父類 base_decoder
中規定的子類必須實現的方法,在初始化完解碼器以後調用,回顧一下:
// base_decoder.cpp
void BaseDecoder::Decode(std::shared_ptr<BaseDecoder> that) {
// 省略無關代碼...
that->InitFFMpegDecoder(env);
that->AllocFrameBuffer();
//子類初始化方法調用
that->Prepare(env);
that->LoopDecode();
that->DoneDecode(env);
// 省略無關代碼...
}
複製代碼
在 Prepare
中,初始化渲染器 InitRender
的先略過,後面詳細再講。
看看數據格式轉化相關的初始化。
// 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);
// 分配內存
m_buf_for_rgb_frame = (uint8_t *) av_malloc(numBytes * sizeof(uint8_t));
// 將內存分配給RgbFrame,並將內存格式化爲三個通道後,分別保存其地址
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
,注意該方法沒有分配緩存內存;
而後經過 av_image_get_buffer_size
方法計算所需內存塊大小,其中
AVPixelFormat DST_FORMAT = AV_PIX_FMT_RGBA
m_dst_w: 爲目標畫面寬度(即畫面顯示時的實際寬度,將經過後續渲染器中具體的窗戶大小計算得出)
m_dst_h:爲目標畫面高度(即畫面顯示時的實際高度,將經過後續渲染器中具體的窗戶大小計算得出)
複製代碼
接着經過 av_malloc
真正分配一塊內存;
最後,經過 av_image_fill_arrays
將獲得的這塊內存給到 AVFrame
,至此,內存分配完成。
// base_decoder.cpp
void VideoDecoder::InitSws() {
// 初始化格式轉換工具
m_sws_ctx = sws_getContext(width(), height(), video_pixel_format(),
m_dst_w, m_dst_h, DST_FORMAT,
SWS_FAST_BILINEAR, NULL, NULL, NULL);
}
複製代碼
這個很簡單,只要將原畫面數據和目標畫面數據的長寬、格式等傳遞進去便可。
在解碼完畢之後,父類會調用子類 Release
方法,以釋放子類中相關的資源。
// 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;
}
}
複製代碼
初始化和資源釋放已經完成,就剩下最後的渲染器配置了。
剛剛上面說過,通常有兩種方式渲染畫面,那麼就先把渲染器先定義好,方便後面擴展。
新建目錄 src/main/cpp/media/render/video
,並建立頭文件 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
複製代碼
該類一樣是純虛類,相似 Java
的 interface
。
這裏只是規定了幾個接口,分別是初始化、渲染、釋放資源。
新建目錄 src/main/cpp/media/render/video/native_render
,並建立頭文件 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;
// 存放輸出到屏幕的緩存數據
ANativeWindow_Buffer m_out_buffer;
// 本地窗口
ANativeWindow *m_native_window = NULL;
//顯示的目標寬
int m_dst_w;
//顯示的目標高
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 ;
};
複製代碼
能夠看到,渲染器中持有一個 Surface
引用,這就是咱們很是熟悉的東西,前面一系列文章中,畫面渲染都是使用了它。
另外還有一個就是本地窗口 ANativeWindow
,只要將 Surface
綁定給 ANativeWindow
,就能夠經過本地窗口實現 Surface
渲染了。
看看渲染器的實現 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);
// 繪製區域的寬高
int windowWidth = ANativeWindow_getWidth(m_native_window);
int windowHeight = ANativeWindow_getHeight(m_native_window);
// 計算目標視頻的寬高
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)
//設置寬高限制緩衝區中的像素數量
ANativeWindow_setBuffersGeometry(m_native_window, windowWidth,
windowHeight, WINDOW_FORMAT_RGBA_8888);
dst_size[0] = m_dst_w;
dst_size[1] = m_dst_h;
}
複製代碼
重點來看 InitRender
方法:
經過 ANativeWindow_fromSurface
將 Surface
綁定給本地窗口;
經過 ANativeWindow_getWidth
ANativeWindow_getHeight
能夠獲取到 Surface
可顯示區域的寬高;
而後,根據原始視頻畫面的寬高 video_width
video_height
以及可現實區域的寬高,進行畫面縮放,能夠計算出最終顯示的畫面的寬高,並賦值給解碼器。
視頻解碼器
v_decoder
在獲取到目標畫面寬高以後,就能夠去初始化數據轉化緩存區的大小了。
最後,經過 ANativeWindow_setBuffersGeometry
設置一下本地窗口緩存區大小,完成初始化。
兩個重要的本地方法:
ANativeWindow_lock
鎖定窗口,並獲取到輸出緩衝區 m_out_buffer
。
ANativeWindow_unlockAndPost
釋放窗口,並將緩衝數據繪製到屏幕上。
// 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:一行能夠保存的內存像素數量*4(即:rgba的位數)
int dstStride = m_out_buffer.stride * 4;
int srcStride = one_frame->line_size;
// 因爲window的stride和幀的stride不一樣,所以須要逐行復制
for (int h = 0; h < m_dst_h; h++) {
memcpy(dst + h * dstStride, one_frame->data + h * srcStride, srcStride);
}
//釋放窗口
ANativeWindow_unlockAndPost(m_native_window);
}
複製代碼
渲染過程看起來很複雜,主要是由於這裏有一個 stride
的概念,指的是一幀畫面每一行數據的寬度大小。
好比這裏的數據格式是
RGBA
,一行畫面的像素是 8 個,那麼總共的stride
寬度就是 8*4 = 32 。
爲何須要轉換呢?緣由是本地窗口的stride
大小可能和視頻畫面數據的stride
不一致,直接將視頻畫面數據給到本地窗口時,可能會致使數據讀取不一致,最終致使花屏。
因此,這裏須要根據本地窗口的 dstStride
和視頻畫面數據的 srcStride
,將數據一行一行復制(memcpy)。
最後來看下,視頻解碼器 v_decoder
中對渲染器的調用
// 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);
}
複製代碼
一是,將渲染設置給視頻解碼器;
二是,調用渲染器的 InitRender
方法初始化渲染器,並得到目標畫面寬高
最後是,調用渲染器 Render
方法,進行渲染。
其中,OneFrame
是自定義類,用來封裝一幀數據相關的內容,知道便可,具體能夠查看【工程源碼】。
以上,完成了 :
基礎解碼器
的封裝 --> 視頻解碼器
的實現;渲染器
的定義 --> 本地渲染窗口
的實現。最後就差把他們整合在一塊兒,實現播放了。
在 src/main/cpp/media
目錄下新建一個播放器 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
複製代碼
播放器持有一個視頻解碼器和一個視頻渲染器,以及一個播放和暫停方法。
// 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中的線程已經使用智能指針,會自動釋放
}
void Player::play() {
if (m_v_decoder != NULL) {
m_v_decoder->GoOn();
}
}
void Player::pause() {
if (m_v_decoder != NULL) {
m_v_decoder->Pause();
}
}
複製代碼
代碼很簡單,就是把解碼器和渲染器關聯起來。
雖然上面完成了各個功能模塊的編寫,可是編譯器不會自動把它們加入編譯。要想讓 C++
代碼加入編譯,須要手動在 CMakeLists.txt
文件中配置,配置的位置和默認的 native-lib.cpp
相同,羅列在後面便可。
# CMakeLists.txt
// 省略無關配置
//......
# 配置目標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
)
// 省略無關配置
//......
複製代碼
若是類只有
.h
頭文件的話,就只寫.h
文件,若是類既有頭文件,又有.cpp
實現文件,則只須要配置.cpp
文件
須要注意的是:在建立好每一個類的時候,就須要將其配置到 CMakeLists.txt
中,不然在編寫代碼的時,可能沒法導入相關的庫頭文件,也就無法經過編譯。
接下來就須要將播放器暴露給 Java
層使用了,這時候就須要用到 JNI
的接口文件 native-lib.cpp
了。
開始編寫 JNI
接口以前,先在 FFmpegActivity
中寫好相應的接口:
// 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 相關接口方法 ----------------------
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: 建立播放器,並返回播放器對象地址
play(player: Int):播放,參數爲播放器對象
pause(player: Int): 暫停,參數爲播放器對象
播放器的建立時機爲 SurfaceView
初始化完成時: 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>
複製代碼
接下來,就根據以上三個接口,在 JNI
中編寫對應的接口。
// 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();
}
}
複製代碼
很簡單,相信你們都看得懂,其實就是初始化一個播放器對象指針,而後返回給 Java
層保存,後面的播放和暫停操做都是 Java
層將這個播放器指針再傳給 JNI
層作具體操做。
代碼不少,可是其實若是看過前面系列原生硬解的文章的話,應該也比較好理解了。
最後,簡單作一下總結吧:
初始化:根據 FFmpeg
提供的一些功能接口,對解碼器作初始化
解碼:經過 FFmpeg
提供的解碼接口進行解碼
轉碼和縮放:經過 FFmpeg
提供的轉碼接口將 YUV 轉換爲 RGBA
渲染:經過 Android
提供的接口將視頻數據渲染到屏幕上
Stride
將數據複製(memcpy)到緩衝區