聲 明
首先,這一系列文章均基于自己的理解和實踐,可能有不對的地方,歡迎大家指正。
其次,這是一個入門系列,涉及的知識也僅限于夠用,深入的知識網上也有許許多多的博文供大家學習了。
最后,寫文章過程中,會借鑒參考其他人分享的文章,會在文章最后列出,感謝這些作者的分享。
碼字不易,轉載請注明出處!
教程代碼:【Github傳送門】 |
---|
目錄
一、Android音視頻硬解碼篇:
二、使用OpenGL渲染視頻畫面篇
- 1,初步了解OpenGL ES
- 2,使用OpenGL渲染視頻畫面
- 3,OpenGL渲染多視頻,實現畫中畫
- 4,深入了解OpenGL之EGL
- 5,OpenGL FBO數據緩沖區
- 6,Android音視頻硬編碼:生成一個MP4
三、Android FFmpeg音視頻解碼篇
- 1,FFmpeg so庫編譯
- 2,Android 引入FFmpeg
- 3,Android FFmpeg視頻解碼播放
- 4,Android FFmpeg+OpenSL ES音頻解碼播放
- 5,Android FFmpeg+OpenGL ES播放視頻
- 6,Android FFmpeg簡單合成MP4:視屏解封與重新封裝
- 7,Android FFmpeg視頻編碼
本文你可以了解到
利用 FFmpeg 對音視頻進行簡單的解封和重新封裝,不涉及解碼和編碼,為下一篇講解如何對編輯好的視頻進行重編碼和封裝做好鋪墊。
一、前言
前面的文章中,對 FFmpg 視頻的解碼,以及如何利用 OpenGL 對視頻進行編輯和渲染,做了詳細的講解,接來非常重要的,就是對編輯好的視頻進行編碼和保存。
當然了,在了解如何編碼之前,先了解如何對編碼好的音視頻進行封裝,會有事半功倍的效果。
在《音視頻解封和封裝:生成一個MP4》中使用了 Android 的原生功能,實現了對音視頻的重打包。FFmpeg 也是同樣的,只不過流程更為繁瑣一些。
二、初始化封裝參數
我們知道,將編碼數據封裝到 Mp4 中,需要知道音視頻編碼相關的參數,比如編碼格式,視頻的寬高,音頻通道數,幀率,比特率等,下面就先看看如何初始化它們。
首先,定義一個打包器 FFRepacker
:
// ff_repack.h
class FFRepack {
private:
const char *TAG = "FFRepack";
AVFormatContext *m_in_format_cxt;
AVFormatContext *m_out_format_cxt;
int OpenSrcFile(char *srcPath);
int InitMuxerParams(char *destPath);
public:
FFRepack(JNIEnv *env,jstring in_path, jstring out_path);
};
初始化過程分為兩個步驟:打開原視頻文件、初始化打包參數。
// ff_repack.cpp
FFRepack::FFRepack(JNIEnv *env, jstring in_path, jstring out_path) {
const char *srcPath = env->GetStringUTFChars(in_path, NULL);
const char *destPath = env->GetStringUTFChars(out_path, NULL);
// 打開原視頻文件,并獲取相關參數
if (OpenSrcFile(srcPath) >= 0) {
// 初始化打包參數
if (InitMuxerParams(destPath)) {
LOGE(TAG, "Init muxer params fail")
}
} else {
LOGE(TAG, "Open src file fail")
}
}
打開原視頻,獲取原視頻參數
代碼很簡單,在使用 FFMpeg 解碼的文章中就已經講解過。如下:
// ff_repack.cpp
int FFRepack::OpenSrcFile(const char *srcPath) {
// 打開文件
if ((avformat_open_input(&m_in_format_cxt, srcPath, 0, 0)) < 0) {
LOGE(TAG, "Fail to open input file")
return -1;
}
// 獲取音視頻參數
if ((avformat_find_stream_info(m_in_format_cxt, 0)) < 0) {
LOGE(TAG, "Fail to retrieve input stream information")
return -1;
}
return 0;
}
初始化打包參數
初始化打包參數稍微復雜一些,主要過程是:
查找原視頻中有哪些音視頻流,并為目標視頻(即重打包視頻文件)添加對應的流通道和初始化對應的編碼參數。
接著,使用已經初始化完畢的上下文,打開目標存儲文件。
最后,往目標文件中,寫入視頻頭部信息。
代碼如下, 主要流程請查看注釋:
//ff_repack.cpp
int FFRepack::InitMuxerParams(const char *destPath) {
// 初始化輸出上下文
if (avformat_alloc_output_context2(&m_out_format_cxt, NULL, NULL, destPath) < 0) {
return -1;
}
// 查找原視頻所有媒體流
for (int i = 0; i < m_in_format_cxt->nb_streams; ++i) {
// 獲取媒體流
AVStream *in_stream = m_in_format_cxt->streams[i];
// 為目標文件創建輸出流
AVStream *out_stream = avformat_new_stream(m_out_format_cxt, NULL);
if (!out_stream) {
LOGE(TAG, "Fail to allocate output stream")
return -1;
}
// 復制原視頻數據流參數到目標輸出流
if (avcodec_parameters_copy(out_stream->codecpar, in_stream->codecpar) < 0) {
LOGE(TAG, "Fail to copy input context to output stream")
return -1;
}
}
// 打開目標文件
if (avio_open(&m_out_format_cxt->pb, destPath, AVIO_FLAG_WRITE) < 0) {
LOGE(TAG, "Could not open output file %s ", destPath);
return -1;
}
// 寫入文件頭信息
if (avformat_write_header(m_out_format_cxt, NULL) < 0) {
LOGE(TAG, "Error occurred when opening output file");
return -1;
} else {
LOGE(TAG, "Write file header success");
}
return 0;
}
以上,初始化已經完畢,下面就可以進行音視頻的解封和重新封裝了。
三、原視頻解封裝
新增一個 Start
方法,用于開啟重打包
// ff_repack.h
class FFRepack {
// 省略其他...
public:
// 省略其他...
void Start();
};
具體實現如下:
// ff_repack.cpp
void FFRepack::Start() {
LOGE(TAG, "Start repacking ....")
AVPacket pkt;
while (1) {
// 讀取數據
if (av_read_frame(m_in_format_cxt, &pkt)) {
LOGE(TAG, "End of video,write trailer")
// 釋放數據幀
av_packet_unref(&pkt);
// 讀取完畢,寫入結尾信息
av_write_trailer(m_out_format_cxt);
break;
}
// 寫入一幀數據
Write(pkt);
}
// 釋放資源
Release();
}
解封依然很簡單,在之前的解碼文章同樣介紹過,主要是將數據讀取到 AVPacket
中。然后調用 Write
方法,將幀數據寫入目標文件中。下面就來看看 Write
方法。
四、目標視頻封裝
增加一個 Write
方法。
// ff_repack.h
class FFRepack {
// 省略其他...
public:
// 省略其他...
void Write(AVPacket pkt);
};
// ff_repacker.cpp
void FFRepack::Write(AVPacket pkt) {
// 獲取數據對應的輸入/輸出流
AVStream *in_stream = m_in_format_cxt->streams[pkt.stream_index];
AVStream *out_stream = m_out_format_cxt->streams[pkt.stream_index];
// 轉換時間基對應的 PTS/DTS
int rounding = (AV_ROUND_NEAR_INF | AV_ROUND_PASS_MINMAX);
pkt.pts = av_rescale_q_rnd(pkt.pts, in_stream->time_base, out_stream->time_base,
(AVRounding)rounding);
pkt.dts = av_rescale_q_rnd(pkt.dts, in_stream->time_base, out_stream->time_base,
(AVRounding)rounding);
pkt.duration = av_rescale_q(pkt.duration, in_stream->time_base, out_stream->time_base);
pkt.pos = -1;
// 將數據寫入目標文件
if (av_interleaved_write_frame(m_out_format_cxt, &pkt) < 0) {
LOGE(TAG, "Error to muxing packet: %x", ret)
}
}
流程很簡單,將該幀數據的 pts
和 dts
、 duration
進行轉換以后,將數據寫入即可。
在寫入數據之前,先獲取了該幀數據所在的流和寫入的數據流。這是因為,在寫入之前,需要對數據的時間進行轉換。
FFmpeg 中的時間單位
我們知道,每一幀音視頻數據都有其對應的時間戳,根據這個時間戳就可以實現對音視頻播放的控制。
FFmpeg
中的時間戳并不是我們實際中的時間,它是一個特別的數值。并且在 FFmpeg
中,還有一個叫 時間基
的概念,時間基
是 FFmpeg
中的時間單位。
[時間戳的值]
乘以
[時間基],才是[實際的時間],并且單位為秒。
換而言之,FFmpeg
的時間戳的值,是隨著 時間基
的不同而變化的。
而 FFmpeg
在不同的階段和不同的封裝格式下也有著不同的時間基,因此,在進行幀數據的封裝時,需要根據各自的時間基進行 “時間戳” 轉換,以保證最終計算得到的實際時間是一致的。
當然了,為了方便轉換 FFmpeg
為我們提供了轉換的方法,并且處理了數據的溢出和取整問題。
av_rescale_q_rnd(int64_t a, AVRational bq,AVRational cq,enum AVRounding rnd)
其內部原理很簡單:return (a × bq / cq)。
即:
x(目標時間戳值) * cq(目標時間基)= a(原時間戳值) * bq(原時間基)
=》=》=》=》=》=》
x = a * bq / cq
當所有數據幀都讀取完畢之后,需要通過 av_write_trailer
寫入結尾信息,這樣文件才算完整,視頻才能正常播放。
五、釋放資源
最后,需要將之前打開的資源進行關閉,避免內存泄漏。
增加一個 Release
方法:
// ff_repack.h
class FFRepack {
// 省略其他...
public:
// 省略其他...
void Release();
};
//ff_repack.cpp
void FFRepack::Release() {
LOGE(TAG, "Finish repacking, release resources")
// 關閉輸入
if (m_in_format_cxt) {
avformat_close_input(&m_in_format_cxt);
}
// 關閉輸出
if (m_out_format_cxt) {
avio_close(m_out_format_cxt->pb);
avformat_free_context(m_out_format_cxt);
}
}
六、調用重打包
新增 JNI 接口
在 native-lib.cpp
中,新增 JNI
接口
// native-lib.cpp
extern "C" {
// 省略其他 ...
JNIEXPORT jint JNICALL
Java_com_cxp_learningvideo_FFRepackActivity_createRepack(JNIEnv *env,
jobject /* this */,
jstring srcPath,
jstring destPath) {
FFRepack *repack = new FFRepack(env, srcPath, destPath);
return (jint) repack;
}
JNIEXPORT void JNICALL
Java_com_cxp_learningvideo_FFRepackActivity_startRepack(JNIEnv *env,
jobject /* this */,
jint repack) {
FFRepack *ffRepack = (FFRepack *) repack;
ffRepack->Start();
}
}
新增頁面
// FFRepackActivity.kt
class FFRepackActivity: AppCompatActivity() {
private var ffRepack: Int = 0
private val srcPath = Environment.getExternalStorageDirectory().absolutePath + "/mvtest.mp4"
private val destPath = Environment.getExternalStorageDirectory().absolutePath + "/mvtest_repack.mp4"
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_ff_repack)
ffRepack = createRepack(srcPath, destPath)
}
fun onStartClick(view: View) {
if (ffRepack != 0) {
thread {
startRepack(ffRepack)
}
}
}
private external fun createRepack(srcPath: String, destPath: String): Int
private external fun startRepack(repack: Int)
companion object {
init {
System.loadLibrary("native-lib")
}
}
}
以上,就是使用 FFmpeg 解封和封裝的過程,比較簡單,主要是為后面視頻編輯、編碼、封裝做好準備。