視頻播放器之解封裝

在之前的文章中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)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容