視頻播放器之解封裝

在之前的文章中FFmpeg的編譯集成也完成了宝惰,這一篇開始視頻播放器處理的第一步:解封裝

解封裝

在解封裝的代碼開始以前脯丝,我們需要引入Log機(jī)制燎含,雖然現(xiàn)在ndk開發(fā)中也能debug了但是有l(wèi)og會更方便。具體怎么引入在我的Android JNI開發(fā)系列之Java與C相互調(diào)用一文最后有方法军援,這里就不再贅述了仅淑。

解封裝這一步是處理視頻數(shù)據(jù)的開始,需要處理以下幾步:

解封裝.jpg

Android的界面比較簡單這里就不寫了胸哥,樣子是這樣的:

ui.png

就兩個按鈕涯竟,第一個按鈕把初始化和打開數(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)然如果有不正確的地方請不吝賜教丁稀。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市倚聚,隨后出現(xiàn)的幾起案子线衫,更是在濱河造成了極大的恐慌,老刑警劉巖惑折,帶你破解...
    沈念sama閱讀 217,185評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件授账,死亡現(xiàn)場離奇詭異,居然都是意外死亡惨驶,警方通過查閱死者的電腦和手機(jī)白热,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,652評論 3 393
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來粗卜,“玉大人屋确,你說我怎么就攤上這事。” “怎么了乍恐?”我有些...
    開封第一講書人閱讀 163,524評論 0 353
  • 文/不壞的土叔 我叫張陵评疗,是天一觀的道長。 經(jīng)常有香客問我茵烈,道長百匆,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,339評論 1 293
  • 正文 為了忘掉前任呜投,我火速辦了婚禮加匈,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘仑荐。我一直安慰自己雕拼,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,387評論 6 391
  • 文/花漫 我一把揭開白布粘招。 她就那樣靜靜地躺著啥寇,像睡著了一般。 火紅的嫁衣襯著肌膚如雪洒扎。 梳的紋絲不亂的頭發(fā)上辑甜,一...
    開封第一講書人閱讀 51,287評論 1 301
  • 那天,我揣著相機(jī)與錄音袍冷,去河邊找鬼磷醋。 笑死,一個胖子當(dāng)著我的面吹牛胡诗,可吹牛的內(nèi)容都是我干的邓线。 我是一名探鬼主播,決...
    沈念sama閱讀 40,130評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼煌恢,長吁一口氣:“原來是場噩夢啊……” “哼骇陈!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起症虑,我...
    開封第一講書人閱讀 38,985評論 0 275
  • 序言:老撾萬榮一對情侶失蹤缩歪,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后谍憔,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體匪蝙,經(jīng)...
    沈念sama閱讀 45,420評論 1 313
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,617評論 3 334
  • 正文 我和宋清朗相戀三年习贫,在試婚紗的時候發(fā)現(xiàn)自己被綠了逛球。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,779評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡苫昌,死狀恐怖颤绕,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤奥务,帶...
    沈念sama閱讀 35,477評論 5 345
  • 正文 年R本政府宣布物独,位于F島的核電站,受9級特大地震影響氯葬,放射性物質(zhì)發(fā)生泄漏挡篓。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,088評論 3 328
  • 文/蒙蒙 一帚称、第九天 我趴在偏房一處隱蔽的房頂上張望官研。 院中可真熱鬧,春花似錦闯睹、人聲如沸戏羽。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,716評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽始花。三九已至,卻和暖如春孩锡,著一層夾襖步出監(jiān)牢的瞬間衙荐,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,857評論 1 269
  • 我被黑心中介騙來泰國打工浮创, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人砌函。 一個月前我還...
    沈念sama閱讀 47,876評論 2 370
  • 正文 我出身青樓斩披,卻偏偏與公主長得像,于是被迫代替她去往敵國和親讹俊。 傳聞我的和親對象是個殘疾皇子垦沉,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,700評論 2 354

推薦閱讀更多精彩內(nèi)容