31.Android Studio下FFmpeg的編譯和使用(五.FFmpeg解封裝)

項目源碼
Android Studio的開發環境已經準備好,接下來開始正式的寫ndk代碼,首先創建一個FFmpeg工具類,添加native方法

import android.view.Surface;
public class FFmpegPlayer {
    static {
        System.loadLibrary("ffmpeg");
    }
    /**
     * 播放視頻
     */
    public native void playVideo(String url,Surface surface);

}

傳入的Surface對象是用於顯示播放界面的,這裏先傳入,後邊再說

然後生成對應的頭文件,之前在eclipse上開發的時候都是通過javah命令生成頭文件後將對應的方法名拷貝到我們的c文件中,這部分的生成方法可以查看之前的博客。現在我們使用的Android Studio3.x可以一鍵生成對應的native方法,將光標選中方法名然後alt+enter選擇create function xxx即可

extern "C"
JNIEXPORT void JNICALL
Java_com_rzm_ffmpegplayer_FFmpegPlayer_playVideo(JNIEnv *env, jobject instance, jstring url_,
                                                 jobject surface) {
    const char *url = env->GetStringUTFChars(url_, 0);

    env->ReleaseStringUTFChars(url_, url);
}

今天來進行的是FFmpeg的第一部分,解封裝

解封裝

一個mp4文件可能是視頻流音頻流字幕流等等多個流的一個結合體,而我們要實現視頻和音頻的播放,就需要將這個結合體拆分開來,單獨的進行視頻播放,音頻播放,實現同步,這是實現播放的一個必要的條件。所以播放的第一步就要進行源文件的解封裝,解封裝涉及到的一些接口如下

av_register_all()

初始化libavformat 並註冊所有的muxers和demuxers以及各種協議,當然,如果你只需要初始化特定的支持組件,那麼單獨調用特定的方法即可,一般直接這樣做一勞永逸

avformat_open_input

打開輸入流(可以是本地流也可以是網絡流,如果是網絡的,那麼需要avformat_network_init()方法支持)並讀取視頻頭信息,視頻頭通常包含一些視頻基本信息,比如說視頻格式,streams的數量等等。注意此時編解碼器未打開,最後必須使用avformat_close_input(&avFormatContext)進行關閉,避免造成泄漏

avformat_network_init()

初始化全局的網絡組件,支持rtfp協議的時候需要打開這個開關,ffmpeg推薦打開這個全局的網絡開關,因爲可以減少單獨對每個單獨回話做設置的開銷

avformat_find_stream_info()

打開流文件之後執行這個方法,讀取媒體文件獲取到流信息,這個方法對於沒有視頻頭的視頻尤其有效(MPEG-2),,此方法執行之後會將讀取到的流信息填充到AVFormatContext中的各個流的軌道上,這樣一來,AVFormatContext->streams[i]中就有信息了.這個方法可以打開部分解碼器並保存在第二個參數AVDictionary中,但是他無法保證打開全部的解碼器,如果存在null那麼也是正常現象,這裏第二個參數我們直接置爲NULL

av_find_best_stream

媒體信息已經獲取到了,接下來需要將視頻流和音頻流區分開來,可以通過av_find_best_stream來獲取流軌道的index,在老版本的ffmpeg上我們是通過遍歷所有的流通過對比codec_type來判斷的,代碼如下

    for(int i = 0; i < avFormatContext->nb_streams; i++){
        AVStream *avStream = avFormatContext->streams[i];
        if (avStream->codecpar->codec_type == AVMEDIA_TYPE_VIDEO){
            //找到視頻index
            videoIndex = i;
        } else if (avStream->codecpar->codec_type == AVMEDIA_TYPE_AUDIO){
            //找到音頻index
            audioIndex = i;            
        }
    }

通過這個方法獲取的方式爲下,驗證一下,你會發現二者結果相同

videoIndex = av_find_best_stream(avFormatContext,AVMEDIA_TYPE_VIDEO,-1,-1,NULL,0);
audioIndex = av_find_best_stream(avFormatContext,AVMEDIA_TYPE_AUDIO,-1,-1,NULL,0);
av_read_frame

返回流的下一幀,這個函數返回文件中存儲的內容。每調用一次,它就將返回一幀數據,這是從文件存儲的內容中切割出來的

av_packet_unref

這個方法會擦除packet空間,不再指向這個packet的緩存空間,另外也會將packet重置爲默認狀態。一幀數據也就是一個ACPacket使用完之後需要回收,否則會造成內存急劇增長,下邊分別是調用這個方法和不調用這個方法的內存變化狀態


調用.png

不調用.png
完整的代碼如下
/**
 * 播放視頻,支持本地和網絡兩種
 */
extern "C"
JNIEXPORT void JNICALL
Java_com_rzm_ffmpegplayer_FFmpegPlayer_playVideo(JNIEnv *env, jobject instance, jstring url_,
                                                 jobject surface) {
    const char *url = env->GetStringUTFChars(url_, 0);

    //初始化解封裝
    av_register_all();
    //初始化全局網絡組件,可選推薦使用,在使用網絡協議的場景中這是必選的(rtfp http)
    avformat_network_init();

    AVFormatContext *avFormatContext = NULL;
    //指定輸入的格式,如果爲NULL,將自動檢測輸入格式,所以可置爲NULL
    //AVInputFormat *fmt = NULL;
    //打開輸入文件,可以是本地視頻或者網絡視頻
    int result = avformat_open_input(&avFormatContext,url,NULL,NULL);

    //打開輸入內容失敗
    if(result != 0){
        LOGE("avformat_open_input failed!:%s",av_err2str(result));
        return;
    }

    //打開輸入成功
    LOGI("avformat_open_input success!:%s",av_err2str(result));

    //讀取媒體文件的分組以獲得流信息。這個對於沒有標題的文件格式(如MPEG)很有用。這個函數還計算實際的幀率在
    //MPEG-2重複的情況下幀模式。
    result = avformat_find_stream_info(avFormatContext,NULL);
    if (result < 0){
        LOGE("avformat_find_stream_info failed: %s",av_err2str(result));
    }

    //獲取到了輸入文件信息,打印一下視頻時長和nb_streams
    LOGI("duration = %lld nb_streams=%d",avFormatContext->duration,avFormatContext->nb_streams);

    //分離音視頻,獲取音視頻在源文件中的streams index
    int videoIndex = 0;
    int audioIndex = 1;
    int fps = 0;
    for(int i = 0; i < avFormatContext->nb_streams; i++){
        AVStream *avStream = avFormatContext->streams[i];
        if (avStream->codecpar->codec_type == AVMEDIA_TYPE_VIDEO){
            //找到視頻index
            videoIndex = i;
            LOGI("video index = %d",videoIndex);
            //FPS是圖像領域中的定義,是指畫面每秒傳輸幀數
            fps = r2d(avStream->avg_frame_rate);

            LOGI("video info ---- fps = %d fps den= %d fps num= %d width=%d height=%d code id=%d format=%d",
                 fps,
                 avStream->avg_frame_rate.den,
                 avStream->avg_frame_rate.num,
                 avStream->codecpar->width,
                 avStream->codecpar->height,
                 avStream->codecpar->codec_id,
                 avStream->codecpar->format
            );

        } else if (avStream->codecpar->codec_type == AVMEDIA_TYPE_AUDIO){
            //找到音頻index
            audioIndex = i;
            LOGI("audio index = %d sampe_rate=%d channels=%d sample_format=%d",
                 audioIndex,
                 avStream->codecpar->sample_rate,
                 avStream->codecpar->channels,
                 avStream->codecpar->format
            );
        }
    }

    //上邊通過遍歷streams音視頻的index,還可以通過提供的接口獲取
    videoIndex = av_find_best_stream(avFormatContext,AVMEDIA_TYPE_VIDEO,-1,-1,NULL,0);
    audioIndex = av_find_best_stream(avFormatContext,AVMEDIA_TYPE_AUDIO,-1,-1,NULL,0);
    LOGI("av_find_best_stream videoIndex=%d audioIndex=%d",videoIndex,audioIndex);

    //讀取幀數據

    //Allocate an AVPacket and set its fields to default values
    //存儲壓縮數據,對於視頻,它通常應該包含一個壓縮幀。對於音頻它可能包含幾個壓縮幀
    AVPacket *avPacket = av_packet_alloc();
    for (;;) {

        //Return the next frame of a stream.
        int read_result = av_read_frame(avFormatContext,avPacket);
        if(read_result != 0){
            //讀取到結尾處,從20秒位置繼續開始播放
            LOGI("讀取到結尾處 %s",av_err2str(read_result));
            //跳轉到指定的position播放,最後一個參數表示
            //int pos = 200000 * r2d(avFormatContext->streams[videoIndex]->time_base);
            //av_seek_frame(avFormatContext,videoIndex,pos,AVSEEK_FLAG_BACKWARD|AVSEEK_FLAG_FRAME );
            //LOGI("avFormatContext->streams[videoIndex]->time_base= %d",avFormatContext->streams[videoIndex]->time_base);
            //continue;
            break;
        }
        LOGW("stream = %d size =%d pts=%lld flag=%d pos = %d",
             avPacket->stream_index,avPacket->size,avPacket->pts,avPacket->flags,avPacket->pos
        );

        //packet使用完成之後執行,否則內存會急劇增長
        //不再引用這個packet指向的空間,並且將packet置爲default狀態
        av_packet_unref(avPacket);
    }

    //關閉上下文
    avformat_close_input(&avFormatContext);
    env->ReleaseStringUTFChars(url_, url);
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章