在之前的文章中FFmpeg的編譯和集成也完成了宝惰,這一篇開始視頻播放器處理的第一步:解封裝
解封裝
在解封裝的代碼開始以前脯丝,我們需要引入Log機(jī)制燎含,雖然現(xiàn)在ndk開發(fā)中也能debug了但是有l(wèi)og會更方便。具體怎么引入在我的Android JNI開發(fā)系列之Java與C相互調(diào)用一文最后有方法军援,這里就不再贅述了仅淑。
解封裝這一步是處理視頻數(shù)據(jù)的開始,需要處理以下幾步:
Android的界面比較簡單這里就不寫了胸哥,樣子是這樣的:
就兩個按鈕涯竟,第一個按鈕把初始化和打開數(shù)據(jù)集成在一步了。
準(zhǔn)備工作
為了方便測試空厌,之后都把測試的方法定義在FFmpegUtil.java文件中庐船,例如這里有初始化,打開數(shù)據(jù)文件和讀取數(shù)據(jù)文件三個方法嘲更,寫出來就是:
public class FFmpegUtil {
static {
System.loadLibrary("native-lib");
}
public static native void init();
public static native void open(String url);
public static native void read();
}
然后實(shí)現(xiàn)都在native-lib.cpp文件中
extern "C"
JNIEXPORT void JNICALL
Java_net_arvin_ffmpegtest_FFmpegUtil_init(JNIEnv *env, jclass type) {
//TODO
}
extern "C"
JNIEXPORT void JNICALL
Java_net_arvin_ffmpegtest_FFmpegUtil_open(JNIEnv *env, jclass type, jstring url_) {
const char *url = env->GetStringUTFChars(url_, 0);
//TODO
env->ReleaseStringUTFChars(url_, url);
}
extern "C"
JNIEXPORT void JNICALL
Java_net_arvin_ffmpegtest_FFmpegUtil_read(JNIEnv *env, jclass type) {
//TODO
}
當(dāng)然實(shí)現(xiàn)還沒有寫筐钟。為了把每塊的代碼分開,所以我們把解封裝的代碼放到一個叫做Demux的cpp文件中赋朦,新建c++class Demux篓冲,然后在CMakeLists文件中添加到庫中(不然找不到文件)。
然后在Demux.h文件中定義三個方法:
class Demux {
public:
virtual void init();
virtual void open(const char *url);
virtual bool read();
}
準(zhǔn)備工作到這里就基本結(jié)束了北发,這三個方法的實(shí)現(xiàn)肯定都在Demux.cpp文件中纹因。
初始化
其實(shí)沒啥說的,就是調(diào)用FFmpeg的api琳拨,首先需要注冊各種封裝器和初始化網(wǎng)絡(luò)瞭恰;當(dāng)然對于網(wǎng)絡(luò)模塊的初始化,是對在線視頻才需要的狱庇。
void Demux::init() {
//注冊所有封裝器
av_register_all();
//初始化網(wǎng)絡(luò)
avformat_network_init();
LOG_I("Register FFmpeg!");
}
這樣寫了惊畏,肯定會提示找不到方法恶耽,所以需要引入頭文件,記住ffmpeg的庫引入都需要加入extern "C"
颜启,當(dāng)然還有Log文件偷俭,如下:
#include "Log.h"
extern "C" {
#include <libavformat/avformat.h>
}
這樣初始化就完成了。
打開數(shù)據(jù)文件
核心方法就是avformat_open_input
缰盏,需要傳入AVFormatContext涌萤,這個上下文對象和文件的url以及其他配置信息,返回值是int口猜,0表示成功负溪,非0可以通過av_strerror轉(zhuǎn)成對應(yīng)的str信息提示。
這一步就能把上下文對象初始化济炎,然后再調(diào)用avformat_find_stream_info
方法就能把常見的文件信息都獲取到川抡,參數(shù)就是傳入上下文和配置字典(可不傳)。
然后帶回讀數(shù)據(jù)要區(qū)分是音頻還是視頻须尚,所以可以通過av_find_best_stream
方法獲取到音頻流的索引和視頻流的索引崖堤。
打開數(shù)據(jù)文件基本就這三個重要的方法,因?yàn)楂@取音頻和視頻信息的時候也能獲取到對應(yīng)的音視頻參數(shù)耐床,所以再在Demux.h中定義了兩個方法密幔,獲取音頻和視頻的參數(shù):
virtual void getVideoParams();
virtual void getAudioParams();
當(dāng)然里邊我們用到的上下文和音視頻流索引也在Demux.h中定義好:
protected:
AVFormatContext *ic;
int videoStream = 0;
int audioStream = 1;
其中AVFormatContext肯定是找不到的,這時候也不要引用FFmpeg的頭文件咙咽,避免耦合老玛,可以定義成struct AVFormatContext;
。
然后實(shí)現(xiàn)的方法如下:
void Demux::open(const char *url) {
LOG_I("open file %s begin", url);
//打開文件
int re = avformat_open_input(&ic, url, 0, 0);
if (re != 0) {
char buff[1024] = {0};
av_strerror(re, buff, sizeof(buff));
LOG_E("Demux open %s failed! error is %s", url, buff);
return;
}
LOG_I("Demux open %s success", url);
//讀取文件信息
re = avformat_find_stream_info(ic, 0);
if (re != 0) {
char buff[1024] = {0};
av_strerror(re, buff, sizeof(buff));
LOG_E("avformat_find_stream_info failed! error is %s", buff);
return;
}
//讀取總時長
int64_t totalMs = ic->duration / (AV_TIME_BASE / 1000);
LOG_I("total ms = %lld", totalMs);
getVideoParams();
getAudioParams();
}
void Demux::getVideoParams() {
if (!ic) {
return;
}
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;
}
videoStream = re;
}
void Demux::getAudioParams() {
if (!ic) {
return;
}
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;
}
audioStream = re;
}
代碼很簡單钧敞,可以看到我們就能獲取到總時長了和音視頻的索引了蜡豹。
讀取數(shù)據(jù)
這一步是解封裝這一步最核心的,因?yàn)橥ㄟ^這一步才獲取到每一幀的數(shù)據(jù)溉苛;核心方法是av_read_frame
镜廉,需要傳入上下文ic,和AVPacket指針愚战;而AVPacket指針的空間需要手動申請和釋放娇唯,不然很容易造成內(nèi)存泄露,所以這一點(diǎn)一定要注意寂玲,自己申請的數(shù)據(jù)一定要清理塔插。
還有一點(diǎn)就是packet返回幀信息中的pts和dps是有一個基數(shù)的,我們把它轉(zhuǎn)成毫秒就好了拓哟,方便之后使用想许,在轉(zhuǎn)換的過程中會涉及到一個AVRational類,是一個分?jǐn)?shù),但是包含分子和分母的流纹,這樣數(shù)據(jù)就更準(zhǔn)確糜烹,一般這個基數(shù)是1000000。當(dāng)然我們使用packet中提供的基數(shù)更準(zhǔn)確漱凝,需要一個將AVRational轉(zhuǎn)成double的方法:
//分?jǐn)?shù)轉(zhuǎn)為浮點(diǎn)數(shù)
static double r2d(AVRational r) {
return r.num == 0 || r.den == 0 ? 0 : (double) r.num / (double) r.den;
}
很簡單疮蹦,就是判斷分母不能為0,open方法的實(shí)現(xiàn)方式如下:
bool Demux::read() {
if (!ic) {
return false;
}
AVPacket *pkt = av_packet_alloc();
int re = av_read_frame(ic, pkt);
if (re != 0) {
av_packet_free(&pkt);
return false;
}
pkt->pts = (long long) (pkt->pts * (1000 * r2d(ic->streams[pkt->stream_index]->time_base)));
if (pkt->stream_index == audioStream) {
LOG_I("read audio size = %d,pts = %lld", pkt->size, pkt->pts);
} else if (pkt->stream_index == videoStream) {
LOG_I("read video size = %d,pts = %lld", pkt->size, pkt->pts);
} else {
av_packet_free(&pkt);
return false;
}
av_packet_free(&pkt);
return true;
}
其中獲取幀數(shù)據(jù)成功之后茸炒,轉(zhuǎn)化pts和dps的時間基數(shù)愕乎,單位編程毫秒,然后再區(qū)分音頻和視頻去打印幀數(shù)據(jù)的大小和pts壁公。當(dāng)然其中av_packet_free
是對AVPacket對象申請空間的釋放妆毕。
這樣這三個方法的實(shí)現(xiàn)就基本完成了,然后我們再回到最開始贮尖,把native-lib中的方法實(shí)現(xiàn)一下,其實(shí)就是調(diào)用demux的方法趁怔。最后再在MainActivity中在點(diǎn)擊不同按鈕調(diào)用FFmpegUtil中的方法即可湿硝。native-lib.cpp代碼如下:
static Demux *demux;
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);
}
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) {
re = demux->read();
}
}
MainActivity中的代碼如下:
@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.btn_init:
initAndOpen();
break;
case R.id.btn_read_data:
readData();
break;
}
}
private void initAndOpen() {
permissionUtil.request("需要讀取讀寫文件權(quán)限", Manifest.permission.WRITE_EXTERNAL_STORAGE,
new PermissionUtil.RequestPermissionListener() {
@Override
public void callback(boolean granted, boolean isAlwaysDenied) {
FFmpegUtil.init();
FFmpegUtil.open("/sdcard/1080.mp4");
}
});
}
private void readData() {
FFmpegUtil.read();
}
這里的permissionUtil是我封裝的對Android6.0以上動態(tài)申請權(quán)限庫,方便使用润努。
這里要打開數(shù)據(jù)文件所以需要文件讀寫權(quán)限关斜,所以在AndroidManifest文件中也要申請權(quán)限:
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
如果是在線視頻文件需要再添加網(wǎng)絡(luò)權(quán)限:
<uses-permission android:name="android.permission.INTERNET"/>
當(dāng)然在初始化部分網(wǎng)絡(luò)初始化就一定要加上。
上邊的代碼比較簡單铺浇,就是FFmpegUtil.open的時候傳入的url是自己本地的文件或者在線的視頻才行痢畜。
到這里解封裝的基本內(nèi)容就完了,還是比較簡單的鳍侣,當(dāng)然如果有不正確的地方請不吝賜教丁稀。