碼字不易,轉載請注明出處!
本文你可以了解到
如何使用 FFmepg 對編輯好的視訊進行重新編碼,生成可以播放的音視訊檔案。
寫在前面
本文是音視訊系列文章的最後一篇了,也是拖了最久的一篇(懶癌發作-_-!!),終于下定決心,把坑填完。
話不多說了,馬上進入正文。
在上一篇文章中,介紹了如何對音視訊檔案進行解封和重新封裝,這個過程不涉及音視訊的解碼和編碼,也就是沒有對音視訊進行編輯,這無法滿足日常的開發需求。
是以,本文将填上編輯過程的空缺,為本系列畫上句号。
一、整體流程說明
在前面的幾篇文章中,我們已經做好了
解碼器
,
OpenGL 渲染器
,是以,編碼的時候,除了需要
編碼器
外,還需要将之前的内容做好整合。下面通過一張圖做一下簡要說明:

子產品
首先可以關注到,這個過程有三個大子產品,也是三個
獨立又互相關聯
的線程,分别負責:
- 原視訊解碼
- OpenGL 畫面渲染
- 目标視訊編碼
資料流向
看下視訊資料是如何流轉的:
- 原視訊經過
解碼後,得到解碼器
資料,經過格式轉換,成為YUV
資料。RGB
-
将解碼器
資料傳遞給RGB
,等待繪制器
使用。OpenGL 渲染器
-
通過内部的線程循環,在适當的時候,調用OpenGL 渲染器
渲染畫面。繪制器
- 畫面繪制完畢以後,得到經過
渲染(編輯過)的畫面,送到OpenGL
進行編碼。編碼器
- 最後,将編碼好的資料,寫入本地檔案。
說明:
本文将主要講音視訊的
知識,由于整個過程涉及到
編碼
、
解碼
OpenGL 渲染
這兩個前面介紹過的知識點,我們将複用之前封裝好的工具,并在一些特殊地方根據編碼的需要做一些适配。
是以接下來在涉及到解碼和OpenGL的地方,至貼出适配的代碼,具體可以檢視之前的文章,或者直接檢視源碼。
二、關于 x264 so 庫編譯和引入
由于
x264
是基于
GPL
開源協定的,而
FFmpeg
預設是基于
LGPL
協定的,當引入
x264
時,由于
GPL
的傳染性,導緻我們的代碼也必須開源,你可以使用
OpenH264
來代替。
這裡仍然使用
x264
來學習相關的編碼過程。
另外,限于篇幅,本文不會介紹關于
x264
的編譯,會另外寫文章介紹。
x264 so 庫的引入和其他 so 引入是一樣的,具體請參考之前的文章,或者檢視源碼中的 CMakeList.txt 。
已經内置了 h264 解碼器,是以如果隻是解碼,并不需要引入
FFmpeg
。
x264
三、封裝編碼器
編碼過程和解碼過程是非常類似的,其實就是解碼的逆過程,是以整個代碼架構流程和解碼器
BaseDecoder
基本是一緻的。
定義 BaseEncoder
BaseEncoder
// BaseEncoder.h
class BaseEncoder: public IEncoder {
private:
// 編碼格式 ID
AVCodecID m_codec_id;
// 線程依附的JVM環境
JavaVM *m_jvm_for_thread = NULL;
// 編碼器
AVCodec *m_codec = NULL;
// 編碼上下文
AVCodecContext *m_codec_ctx = NULL;
// 編碼資料包
AVPacket *m_encoded_pkt = NULL;
// 寫入Mp4的輸入流索引
int m_encode_stream_index = 0;
// 原資料時間基
AVRational m_src_time_base;
// 緩沖隊列
std::queue<OneFrame *> m_src_frames;
// 操作資料鎖
std::mutex m_frames_lock;
// 狀态回調
IEncodeStateCb *m_state_cb = NULL;
bool Init();
/**
* 循環拉去已經編碼的資料,直到沒有資料或者編碼完畢
* @return true 編碼結束;false 編碼未完成
*/
bool DrainEncode();
/**
* 編碼一幀資料
* @return 錯誤資訊
*/
int EncodeOneFrame();
// 建立編碼線程
void CreateEncodeThread();
// 解碼靜态方法,給線程調用
static void Encode(std::shared_ptr<BaseEncoder> that);
void OpenEncoder();
// 循環編碼
void LoopEncode();
void DoRelease();
// 省略一些非重點代碼(具體請檢視源碼)
// .......
protected:
// Mp4 封裝器
Mp4Muxer *m_muxer = NULL;
//-------------子類需要複寫的方法 begin-----------
// 初始化編碼參數(上下文)
virtual void InitContext(AVCodecContext *codec_ctx) = 0;
// 配置Mp4 混淆通道資訊
virtual int ConfigureMuxerStream(Mp4Muxer *muxer, AVCodecContext *ctx) = 0;
// 處理一幀資料
virtual AVFrame* DealFrame(OneFrame *one_frame) = 0;
// 釋放資源
virtual void Release() = 0;
virtual const char *const LogSpec() = 0;
//-------------子類需要複寫的方法 end-----------
public:
BaseEncoder(JNIEnv *env, Mp4Muxer *muxer, AVCodecID codec_id);
// 壓入一幀待編碼資料(由外部調用)
void PushFrame(OneFrame *one_frame) override ;
// 判斷是否緩沖資料過多,用于控制緩沖隊列大小
bool TooMuchData() override {
return m_src_frames.size() > 100;
}
// 設定編碼狀态監聽器
void SetStateReceiver(IEncodeStateCb *cb) override {
this->m_state_cb = cb;
}
};
複制
編碼器定義并不複雜,無非就是編碼需要用到的編碼器
m_codec
、解碼上下文
m_codec_id
等,以及封裝對應的函數方法來拆分編碼過程中的幾個步驟。這裡主要強調幾點:
- 控制編碼緩沖隊列大小
由于編碼過程中,編碼速度遠遠小于解碼速度,是以需要控制緩沖隊列大小,避免大量的資料堆積,導緻内容溢出或申請記憶體失敗問題。
- 時間戳轉換
時間戳轉換在上篇文章中已經有說明,具體請檢視上篇文章。總之,由于原視訊和目标視訊時間基是不一樣的,是以需要對時間戳進行轉換,才能保證編碼儲存後的時間是正常的。
- 確定 MP4 軌道索引是正确的
MP4 有音頻和視訊兩個軌道,需要在寫入的時候,對應好,具體檢視代碼中的 m_encode_stream_index
。
實作 BaseEncoder
BaseEncoder
初始化
// BaseEncoder.cpp
BaseEncoder::BaseEncoder(JNIEnv *env, Mp4Muxer *muxer, AVCodecID codec_id)
: m_muxer(muxer),
m_codec_id(codec_id) {
if (Init()) {
env->GetJavaVM(&m_jvm_for_thread);
CreateEncodeThread();
}
}
bool BaseEncoder::Init() {
// 1. 查找編碼器
m_codec = avcodec_find_encoder(m_codec_id);
if (m_codec == NULL) {
LOGE(TAG, "Fail to find encoder, code id is %d", m_codec_id)
return false;
}
// 2. 配置設定編碼上下文
m_codec_ctx = avcodec_alloc_context3(m_codec);
if (m_codec_ctx == NULL) {
LOGE(TAG, "Fail to alloc encoder context")
return false;
}
// 3. 初始化編碼資料包
m_encoded_pkt = av_packet_alloc();
av_init_packet(m_encoded_pkt);
return true;
}
void BaseEncoder::CreateEncodeThread() {
// 使用智能指針,線程結束時,自動删除本類指針
std::shared_ptr<BaseEncoder> that(this);
std::thread t(Encode, that);
t.detach();
}
複制
編碼需要兩個參數,
m_muxer
和
m_codec_id
,既: MP4 混合器和編碼格式ID。
其中,編碼格式 ID 根據音頻和視訊需要來設定,比如視訊 H264 為:
AV_CODEC_ID_H264
,音頻 AAC 為:
AV_CODEC_ID_AAC
。
接着,調用
Init()
方法:
- 根據編碼格式 ID 查找編碼器
- 配置設定編碼上下文
- 初始化編碼資料包
最後,建立編碼線程。
封裝編碼流程
// BaseEncoder.cpp
void BaseEncoder::Encode(std::shared_ptr<BaseEncoder> that) {
JNIEnv * env;
//将線程附加到虛拟機,并擷取env
if (that->m_jvm_for_thread->AttachCurrentThread(&env, NULL) != JNI_OK) {
LOG_ERROR(that->TAG, that->LogSpec(), "Fail to Init encode thread");
return;
}
that->OpenEncoder(); // 1
that->LoopEncode(); // 2
that->DoRelease(); // 3
//解除線程和jvm關聯
that->m_jvm_for_thread->DetachCurrentThread();
}
複制
過程和解碼非常類似。
第1步,打開編碼器
// BaseEncoder.cpp
void BaseEncoder::OpenEncoder() {
// 調用子類方法,根據音頻和視訊的不同,初始化編碼上下文
InitContext(m_codec_ctx);
int ret = avcodec_open2(m_codec_ctx, m_codec, NULL);
if (ret < 0) {
LOG_ERROR(TAG, LogSpec(), "Fail to open encoder : %d", m_codec);
return;
}
m_encode_stream_index = ConfigureMuxerStream(m_muxer, m_codec_ctx);
}
複制
第2步,開啟編碼循環
編碼的核心方法隻有兩個:
avcodec_send_frame
: 資料發到編碼隊列
avcodec_receive_packet
: 接收編碼好的資料
編碼過程主要有 5 個步驟:
- 從緩沖隊列中擷取待解碼資料
- 将原始資料交給子類處理(音頻和視訊根據自己的需求處理)
- 通過
将資料發送到編碼器編碼avcodec_send_frame
- 将編碼好的資料抽取出來
還有一點,既第 5 點,重新發送資料。
需要說明一下這裡采取的
雙循環
編碼邏輯:除了最外層的
while(tue)
循環以外,裡面還有一個
while (m_src_frames.size() > 0)
循環。
在緩沖隊列有資料,并且 FFmpeg 内部編碼隊列未滿
的情況下,會不斷地往
FFmpeg
發送資料,直到發現
FFmpeg
編碼傳回
AVERROR(EAGAIN)
,則說明内部隊列已滿,需要先将編碼的資料抽取出來,也就是調用
DrainEncode()
方法。
還有一點需要說明的是:如何判讀所有資料已經都發送給編碼器了?
這裡通過
one_frame->line_size
來判斷。
當監聽到解碼器通知解碼完成的時候,則把一個空的幀資料
的
OneFrame
設定為 ,并壓入緩沖隊列。
line_size
拿到這個空資料幀時,往
BaseEncoder
的
FFmpeg
發送一個
avcodec_send_frame()
資料,則
NULL
會自動結束編碼。
FFmpeg
具體請看以下代碼:
// BaseEncoder.cpp
void BaseEncoder::LoopEncode() {
if (m_state_cb != NULL) {
m_state_cb->EncodeStart();
}
while (true) {
if (m_src_frames.size() == 0) {
Wait();
}
while (m_src_frames.size() > 0) {
// 1. 擷取待解碼資料
m_frames_lock.lock();
OneFrame *one_frame = m_src_frames.front();
m_src_frames.pop();
m_frames_lock.unlock();
AVFrame *frame = NULL;
if (one_frame->line_size != 0) {
m_src_time_base = one_frame->time_base;
// 2. 子類處理資料
frame = DealFrame(one_frame);
delete one_frame;
if (m_state_cb != NULL) {
m_state_cb->EncodeSend();
}
if (frame == NULL) {
continue;
}
} else { //如果資料長度為0,說明編碼已經結束,壓入空frame,使編碼器進入結束狀态
delete one_frame;
}
// 3. 将資料發送到編碼器
int ret = avcodec_send_frame(m_codec_ctx, frame);
switch (ret) {
case AVERROR_EOF:
LOG_ERROR(TAG, LogSpec(), "Send frame finish [AVERROR_EOF]")
break;
case AVERROR(EAGAIN): //編碼編碼器已滿,先取出已編碼資料,再嘗試發送資料
while (ret == AVERROR(EAGAIN)) {
LOG_ERROR(TAG, LogSpec(), "Send frame error[EAGAIN]: %s", av_err2str(AVERROR(EAGAIN)));
// 4. 将編碼好的資料榨幹
if (DrainEncode()) return; //編碼結束
// 5. 重新發送資料
ret = avcodec_send_frame(m_codec_ctx, frame);
}
break;
case AVERROR(EINVAL):
LOG_ERROR(TAG, LogSpec(), "Send frame error[EINVAL]: %s", av_err2str(AVERROR(EINVAL)));
break;
case AVERROR(ENOMEM):
LOG_ERROR(TAG, LogSpec(), "Send frame error[ENOMEM]: %s", av_err2str(AVERROR(ENOMEM)));
break;
default:
break;
}
if (ret != 0) break;
}
if (DrainEncode()) break; //編碼結束
}
}
複制
接下來看下上面提到的
DrainEncode()
方法:
// BaseEncoder.cpp
bool BaseEncoder::DrainEncode() {
int state = EncodeOneFrame();
while (state == 0) {
state = EncodeOneFrame();
}
return state == AVERROR_EOF;
}
int BaseEncoder::EncodeOneFrame() {
int state = avcodec_receive_packet(m_codec_ctx, m_encoded_pkt);
switch (state) {
case AVERROR_EOF: //解碼結束
LOG_ERROR(TAG, LogSpec(), "Encode finish")
break;
case AVERROR(EAGAIN): //編碼還未完成,待會再來
LOG_INFO(TAG, LogSpec(), "Encode error[EAGAIN]: %s", av_err2str(AVERROR(EAGAIN)));
break;
case AVERROR(EINVAL):
LOG_ERROR(TAG, LogSpec(), "Encode error[EINVAL]: %s", av_err2str(AVERROR(EINVAL)));
break;
case AVERROR(ENOMEM):
LOG_ERROR(TAG, LogSpec(), "Encode error[ENOMEM]: %s", av_err2str(AVERROR(ENOMEM)));
break;
default: // 成功擷取到一幀編碼好的資料,寫入 MP4
//将視訊pts/dts轉換為容器pts/dts
av_packet_rescale_ts(m_encoded_pkt, m_src_time_base,
m_muxer->GetTimeBase(m_encode_stream_index));
if (m_state_cb != NULL) {
m_state_cb->EncodeFrame(m_encoded_pkt->data);
long cur_time = (long)(m_encoded_pkt->pts*av_q2d(m_muxer->GetTimeBase(m_encode_stream_index))*1000);
m_state_cb->EncodeProgress(cur_time);
}
m_encoded_pkt->stream_index = m_encode_stream_index;
m_muxer->Write(m_encoded_pkt);
break;
}
av_packet_unref(m_encoded_pkt);
return state;
}
複制
同樣是一個
while
循環,根據接收資料的狀态來判斷是否結束循環。
主要邏輯在
EncodeOneFrame()
中,通過
avcodec_receive_packet()
擷取
FFmpeg
中已經完成編碼的資料,如果該方法傳回
說明擷取成功,可以将資料寫入
MP4
中。
EncodeOneFrame()
傳回的就是
avcodec_receive_packet
的傳回值,那麼當其為
時,循環擷取下一幀資料,直到傳回值為
AVERROR(EAGAIN)
或
AVERROR_EOF
,既:沒有資料 或 編碼結束。
如此,通過以上幾個循環,不斷往編碼器塞入資料,和拉取資料,直到完成所有資料編碼,結束編碼。
第3步,結束編碼,釋放資源
完成編碼後,需要釋放相關的資源
// BaseEncoder.cpp
void BaseEncoder::DoRelease() {
if (m_encoded_pkt != NULL) {
av_packet_free(&m_encoded_pkt);
m_encoded_pkt = NULL;
}
if (m_codec_ctx != NULL) {
avcodec_close(m_codec_ctx);
avcodec_free_context(&m_codec_ctx);
}
// 調用子類方法,釋放子類資源
Release();
if (m_state_cb != NULL) {
m_state_cb->EncodeFinish();
}
}
複制
封裝視訊編碼器
視訊編碼器繼承自上面定義好的基礎編碼器
BaseEncoder
。
// VideoEncoder.h
class VideoEncoder: public BaseEncoder {
private:
const char * TAG = "VideoEncoder";
// 視訊格式轉化工具
SwsContext *m_sws_ctx = NULL;
// 一陣 YUV 資料
AVFrame *m_yuv_frame = NULL;
// 目标視訊寬高
int m_width = 0, m_height = 0;
void InitYUVFrame();
protected:
const char *const LogSpec() override {
return "視訊";
};
void InitContext(AVCodecContext *codec_ctx) override;
int ConfigureMuxerStream(Mp4Muxer *muxer, AVCodecContext *ctx) override;
AVFrame* DealFrame(OneFrame *one_frame) override;
void Release() override;
public:
VideoEncoder(JNIEnv *env, Mp4Muxer *muxer, int width, int height);
};
複制
具體實作:
1. 構造方法:
// VideoEncoder.cpp
VideoEncoder::VideoEncoder(JNIEnv *env, Mp4Muxer *muxer, int width, int height)
: BaseEncoder(env, muxer, AV_CODEC_ID_H264),
m_width(width),
m_height(height) {
m_sws_ctx = sws_getContext(width, height, AV_PIX_FMT_RGBA,
width, height, AV_PIX_FMT_YUV420P, SWS_FAST_BILINEAR,
NULL, NULL, NULL);
}
複制
這裡根據目标輸出視訊的寬高,原格式(OpenGL輸出的RGBA資料)/目标格式(YUV),初始化格式轉換器,這個與解碼剛好是相反的過程。
2. 編碼參數初始化:
2.1 初始化上下文和子類内部資料,主要時配置編碼視訊的
寬高
、
碼率
、
幀率
、
時間基
等。
還有一個比較重要的參數就是
qmin
和
qmax
,其值範圍為 [0~51],用于配置編碼畫面品質,值越大,畫面品質越低,視訊檔案越小。可以跟自己的需求配置。
還有就是
InitYUVFrame()
申請轉碼需要用到的
YUV
資料記憶體空間。
// VideoEncoder.cpp
void VideoEncoder::InitContext(AVCodecContext *codec_ctx) {
codec_ctx->bit_rate = 3*m_width*m_height;
codec_ctx->width = m_width;
codec_ctx->height = m_height;
//把1秒鐘分成fps個機關
codec_ctx->time_base = {1, ENCODE_VIDEO_FPS};
codec_ctx->framerate = {ENCODE_VIDEO_FPS, 1};
//畫面組大小
codec_ctx->gop_size = 50;
//沒有B幀
codec_ctx->max_b_frames = 0;
codec_ctx->pix_fmt = AV_PIX_FMT_YUV420P;
codec_ctx->thread_count = 8;
av_opt_set(codec_ctx->priv_data, "preset", "ultrafast", 0);
av_opt_set(codec_ctx->priv_data, "tune", "zerolatency", 0);
//這是量化範圍設定,其值範圍為0~51,
//越小品質越高,需要的比特率越大,0為無損編碼
codec_ctx->qmin = 28;
codec_ctx->qmax = 50;
//全局的編碼資訊
codec_ctx->flags |= AV_CODEC_FLAG_GLOBAL_HEADER;
InitYUVFrame();
LOGI(TAG, "Init codec context success")
}
void VideoEncoder::InitYUVFrame() {
//設定YUV輸出空間
m_yuv_frame = av_frame_alloc();
m_yuv_frame->format = AV_PIX_FMT_YUV420P;
m_yuv_frame->width = m_width;
m_yuv_frame->height = m_height;
//配置設定空間
int ret = av_frame_get_buffer(m_yuv_frame, 0);
if (ret < 0) {
LOGE(TAG, "Fail to get yuv frame buffer");
}
}
複制
2.2 根據解碼器資訊,寫入對應的 MP4 軌道資訊。
// VideoEncoder.cpp
int VideoEncoder::ConfigureMuxerStream(Mp4Muxer *muxer, AVCodecContext *ctx) {
return muxer->AddVideoStream(ctx);
}
複制
3. 處理資料
還記得父類定義的子類資料處理方法嗎?
視訊編碼器需要将
OpenGL
輸出到
RGBA
資料轉化為
YUV
資料,才能送進編碼器編碼。
// VideoEncoder.cpp
AVFrame* VideoEncoder::DealFrame(OneFrame *one_frame) {
uint8_t *in_data[AV_NUM_DATA_POINTERS] = { 0 };
in_data[0] = one_frame->data;
int src_line_size[AV_NUM_DATA_POINTERS] = { 0 };
src_line_size[0] = one_frame->line_size;
int h = sws_scale(m_sws_ctx, in_data, src_line_size, 0, m_height,
m_yuv_frame->data, m_yuv_frame->linesize);
if (h <= 0) {
LOGE(TAG, "轉碼出錯");
return NULL;
}
m_yuv_frame->pts = one_frame->pts;
return m_yuv_frame;
}
複制
4. 釋放子類資源
編碼結束後,父類回調子類方法,方法資源,通知
Mp4Muxer
結束視訊通道寫入。
// VideoEncoder.cpp
void VideoEncoder::Release() {
if (m_yuv_frame != NULL) {
av_frame_free(&m_yuv_frame);
m_yuv_frame = NULL;
}
if (m_sws_ctx != NULL) {
sws_freeContext(m_sws_ctx);
m_sws_ctx = NULL;
}
// 結束視訊通道資料寫入
m_muxer->EndVideoStream();
}
複制
封裝音頻編碼器
音頻編碼器基本視訊是一樣的,隻是參數配置有所不同,直接來看實作就好。
正常的音頻參數配置:比特率,編碼格式,通道數量等
重點看下
InitFrame()
方法,這裡需要通過通道數、編碼格式等,借助
av_samples_get_buffer_size()
方法,計算用來儲存目标幀資料的記憶體大小。
// AudioEncoder.cpp
AudioEncoder::AudioEncoder(JNIEnv *env, Mp4Muxer *muxer)
: BaseEncoder(env, muxer, AV_CODEC_ID_AAC) {
}
void AudioEncoder::InitContext(AVCodecContext *codec_ctx) {
codec_ctx->codec_type = AVMEDIA_TYPE_AUDIO;
codec_ctx->sample_fmt = ENCODE_AUDIO_DEST_FORMAT;
codec_ctx->sample_rate = ENCODE_AUDIO_DEST_SAMPLE_RATE;
codec_ctx->channel_layout = ENCODE_AUDIO_DEST_CHANNEL_LAYOUT;
codec_ctx->channels = ENCODE_AUDIO_DEST_CHANNEL_COUNTS;
codec_ctx->bit_rate = ENCODE_AUDIO_DEST_BIT_RATE;
InitFrame();
}
void AudioEncoder::InitFrame() {
m_frame = av_frame_alloc();
m_frame->nb_samples = 1024;
m_frame->format = ENCODE_AUDIO_DEST_FORMAT;
m_frame->channel_layout = ENCODE_AUDIO_DEST_CHANNEL_LAYOUT;
int size = av_samples_get_buffer_size(NULL, ENCODE_AUDIO_DEST_CHANNEL_COUNTS, m_frame->nb_samples,
ENCODE_AUDIO_DEST_FORMAT, 1);
uint8_t *frame_buf = (uint8_t *) av_malloc(size);
avcodec_fill_audio_frame(m_frame, ENCODE_AUDIO_DEST_CHANNEL_COUNTS, ENCODE_AUDIO_DEST_FORMAT,
frame_buf, size, 1);
}
int AudioEncoder::ConfigureMuxerStream(Mp4Muxer *muxer, AVCodecContext *ctx) {
return muxer->AddAudioStream(ctx);
}
AVFrame* AudioEncoder::DealFrame(OneFrame *one_frame) {
m_frame->pts = one_frame->pts;
memcpy(m_frame->data[0], one_frame->data, 4096);
memcpy(m_frame->data[1], one_frame->ext_data, 4096);
return m_frame;
}
void AudioEncoder::Release() {
m_muxer->EndAudioStream();
}
複制
最後,
DealFrame
需要将
one_frame
中儲存的左右聲道的資料複制到
m_frame
申請的記憶體中,并傳回給
父類
送到編碼器編碼。
四、擷取 OpenGL 渲染的視訊資料
我們知道,視訊資料經過 OpenGL 編輯以後,是無法直接送到編碼器進行編碼的,需要通過
OpenGL
的
glReadPixels
方法來擷取。
下面就改造一下原來定義的
OpenGLRender
來實作。
完整代碼請檢視工程源碼。
在渲染方法
Render()
中,增加擷取的畫面的方法:
// OpenGLRender.cpp
void OpenGLRender::Render() {
if (RENDERING == m_state) {
m_drawer_proxy->Draw();
m_egl_surface->SwapBuffers();
if (m_need_output_pixels && m_pixel_receiver != NULL) {//輸出畫面rgba
m_need_output_pixels = false;
Render(); //再次渲染最新的畫面
size_t size = m_window_width * m_window_height * 4 * sizeof(uint8_t);
uint8_t *rgb = (uint8_t *) malloc(size);
if (rgb == NULL) {
realloc(rgb, size);
LOGE(TAG, "記憶體配置設定失敗: %d", rgb)
}
glReadPixels(0, 0, m_window_width, m_window_height, GL_RGBA, GL_UNSIGNED_BYTE, rgb);
// 将資料發送出去
m_pixel_receiver->ReceivePixel(rgb);
}
}
}
複制
增加一個請求方法,用于通知
OpenGLRender
将資料輸發送出來:
// OpenGLRender.cpp
void OpenGLRender::RequestRgbaData() {
m_need_output_pixels = true;
}
複制
原理很簡單,在解碼器解碼一幀資料送入渲染以後,就馬上通知
OpenGL
将畫面發送出來。
OpenGLRender
當然了,還需要定義一個接收器:
// OpenGLPixelReceiver.h
class OpenGLPixelReceiver {
public:
virtual void ReceivePixel(uint8_t *rgba) = 0;
};
複制
五、MP4 封裝器
該部分内容基本就是上一篇文章的定義的重打包
FFRepack
工具的重新封裝,這裡不再贅述,請檢視上一篇文章,或源碼。
// Mp4Muxer.cpp
void Mp4Muxer::Init(JNIEnv *env, jstring path) {
const char *u_path = env->GetStringUTFChars(path, NULL);
int len = strlen(u_path);
m_path = new char[len];
strcpy(m_path, u_path);
//建立輸出上下文
avformat_alloc_output_context2(&m_fmt_ctx, NULL, NULL, m_path);
// 釋放引用
env->ReleaseStringUTFChars(path, u_path);
}
int Mp4Muxer::AddVideoStream(AVCodecContext *ctx) {
int stream_index = AddStream(ctx);
m_video_configured = true;
Start();
return stream_index;
}
int Mp4Muxer::AddAudioStream(AVCodecContext *ctx) {
int stream_index = AddStream(ctx);
m_audio_configured = true;
Start();
return stream_index;
}
int Mp4Muxer::AddStream(AVCodecContext *ctx) {
AVStream *video_stream = avformat_new_stream(m_fmt_ctx, NULL);
avcodec_parameters_from_context(video_stream->codecpar, ctx);
video_stream->codecpar->codec_tag = 0;
return video_stream->index;
}
void Mp4Muxer::Start() {
if (m_video_configured && m_audio_configured) {
av_dump_format(m_fmt_ctx, 0, m_path, 1);
//打開檔案輸入
int ret = avio_open(&m_fmt_ctx->pb, m_path, AVIO_FLAG_WRITE);
if (ret < 0) {
LOGE(TAG, "Open av io fail")
return;
} else {
LOGI(TAG, "Open av io: %s", m_path)
}
//寫入頭部資訊
ret = avformat_write_header(m_fmt_ctx, NULL);
if (ret < 0) {
LOGE(TAG, "Write header fail")
return;
} else {
LOGI(TAG, "Write header success")
}
}
}
void Mp4Muxer::Write(AVPacket *pkt) {
int ret = av_interleaved_write_frame(m_fmt_ctx, pkt);
// uint64_t time = uint64_t (pkt->pts*av_q2d(GetTimeBase(pkt->stream_index))*1000);
// LOGE(TAG, "Write one frame pts: %lld, ret = %s", time , av_err2str(ret))
}
void Mp4Muxer::EndAudioStream() {
LOGI(TAG, "End audio stream")
m_audio_end = true;
Release();
}
void Mp4Muxer::EndVideoStream() {
LOGI(TAG, "End video stream")
m_video_end = true;
Release();
}
void Mp4Muxer::Release() {
if (m_video_end && m_audio_end) {
if (m_fmt_ctx) {
//寫入檔案尾部
av_write_trailer(m_fmt_ctx);
//關閉輸出IO
avio_close(m_fmt_ctx->pb);
//釋放資源
avformat_free_context(m_fmt_ctx);
m_fmt_ctx = NULL;
}
delete [] m_path;
LOGI(TAG, "Muxer Release")
if (m_mux_finish_cb) {
m_mux_finish_cb->OnMuxFinished();
}
}
}
複制
六、整合調用
有了以上工具的定義和封裝,加上之前的解碼器和渲染器,就萬事俱備,隻欠東風了!
我們需要将他們整合在一起,串聯起整個【解碼--編輯--編碼--寫入MP4】流程。
定義合成器
Synthesizer
。
初始化
// Synthesizer.cpp
// 這裡直接寫死視訊寬高了, 需要根據自己的需求動态配置
static int WIDTH = 1920;
static int HEIGHT = 1080;
Synthesizer::Synthesizer(JNIEnv *env, jstring src_path, jstring dst_path) {
// 封裝器
m_mp4_muxer = new Mp4Muxer();
m_mp4_muxer->Init(env, dst_path);
m_mp4_muxer->SetMuxFinishCallback(this);
// --------------------------視訊配置--------------------------
// 【視訊編碼器】
m_v_encoder = new VideoEncoder(env, m_mp4_muxer, WIDTH, HEIGHT);
m_v_encoder->SetStateReceiver(this);
// 【繪制器】
m_drawer_proxy = new DefDrawerProxyImpl();
VideoDrawer *drawer = new VideoDrawer();
m_drawer_proxy->AddDrawer(drawer);
// 【OpenGL 渲染器】
m_gl_render = new OpenGLRender(env, m_drawer_proxy);
// 設定離屏渲染畫面寬高
m_gl_render->SetOffScreenSize(WIDTH, HEIGHT);
// 接收經過(編輯)渲染的畫面資料
m_gl_render->SetPixelReceiver(this);
// 【視訊解碼器】
m_video_decoder = new VideoDecoder(env, src_path, true);
m_video_decoder->SetRender(drawer);
// 監聽解碼狀态
m_video_decoder->SetStateReceiver(this);
//--------------------------音頻配置--------------------------
// 【音頻編碼器】
m_a_encoder = new AudioEncoder(env, m_mp4_muxer);
// 監聽編碼狀态
m_a_encoder->SetStateReceiver(this);
// 【音頻解碼器】
m_audio_decoder = new AudioDecoder(env, src_path, true);
// 監聽解碼狀态
m_audio_decoder->SetStateReceiver(this);
}
複制
可以看到,解碼流程和以前幾乎時一模一樣的,三個不一樣的地方是:
- 需要告訴解碼器,這是合成過程,無需在解碼後加入時間同步。
- OpenGL 渲染是離屏渲染,需要設定渲染尺寸
- 音頻無需渲染到 OpenSL 中,直接發送出來壓入編碼即可。
啟動
初始化完畢後,解碼器進入等待,需要外面觸發進入循環解碼流程。
// Synthesizer.cpp
void Synthesizer::Start() {
m_video_decoder->GoOn();
m_audio_decoder->GoOn();
}
複制
當調用了
BaseDecoder
的
GoOn()
方法以後,整個【解碼-->編碼】流程将被啟動。
而将它們粘合起來的,就是解碼器的狀态回調方法
DecodeOneFrame()
。
// Synthesizer.cpp
bool Synthesizer::DecodeOneFrame(IDecoder *decoder, OneFrame *frame) {
if (decoder == m_video_decoder) {
// 等待上一幀畫面資料壓入編碼緩沖隊列
while (m_cur_v_frame) {
av_usleep(2000); // 2ms
}
m_cur_v_frame = frame;
m_gl_render->RequestRgbaData();
return m_v_encoder->TooMuchData();
} else {
m_cur_a_frame = frame;
m_a_encoder->PushFrame(frame);
return m_a_encoder->TooMuchData();
}
}
void Synthesizer::ReceivePixel(uint8_t *rgba) {
OneFrame *rgbFrame = new OneFrame(rgba, m_cur_v_frame->line_size,
m_cur_v_frame->pts, m_cur_v_frame->time_base);
m_v_encoder->PushFrame(rgbFrame);
// 清空上一幀資料資訊
m_cur_v_frame = NULL;
}
複制
當接收到解碼器的一幀資料後,
- 如果是音頻資料,直接将資料通過
的BaseDecoder
方法壓入隊列。PushFrame()
- 如果是視訊資料,将目前幀資料資訊儲存下來,并通知
将畫面資料發送出來。在OpenGLRender
方法中接收到畫面資料後,将資料ReceivePixel()
到視訊編碼器中。PushFrame()
直到解碼完畢,在
DecodeFinish()
方法中,壓入空資料幀,通知編碼器結束編碼。
// Synthesizer.cpp
void Synthesizer::DecodeFinish(IDecoder *decoder) {
// 編碼結束,壓入一幀空資料,通知編碼器結束編碼
if (decoder == m_video_decoder) {
m_v_encoder->PushFrame(new OneFrame(NULL, 0, 0, AVRational{1, 25}, NULL));
} else {
m_a_encoder->PushFrame(new OneFrame(NULL, 0, 0, AVRational{1, 25}, NULL));
}
}
void Synthesizer::EncodeFinish() {
LOGI("Synthesizer", "EncodeFinish ...");
}
void Synthesizer::OnMuxFinished() {
LOGI("Synthesizer", "OnMuxFinished ...");
m_gl_render->Stop();
if (m_mp4_muxer != NULL) {
delete m_mp4_muxer;
}
m_drawer_proxy = NULL;
}
複制
至此,整個流程就完整了!!!