NDK之FFmpeg視頻解碼播放

前言

代碼github地址https://github.com/ccj659/NDK-FFmpeg-master

FFmpeg庫簡介

  • avcodec:編解碼,包含
  • avformate: 封裝格式處理
  • avfilter:濾鏡特效處理
  • avdevice:輸入輸出設(shè)備
  • avutil:工具庫
  • swresample:音頻采樣處理
  • swscale:視頻像素格式轉(zhuǎn)換,縮放等

FFmpeg解碼流程

  1. av_register_all(); //注冊所有組件。
  2. AVFormatContext //獲取上下文等信息//是封裝格式上下文結(jié)構(gòu)體,統(tǒng)領(lǐng)全局,保存視頻文件的封裝格式信息
  3. avformat_open_input(&pFormCtx,input_cstr,NULL,NULL) //打開輸入文件
  4. avformat_find_stream_info(pFormCtx,NULL)////獲取文件信息,
  5. AVStream,AVCodecContext獲取流索引位置
  6. avcodec_find_decoder(avCodeCtx->codec_id)////查找解碼器。
  7. avcodec_open2(avCodeCtx,avCode,NULL)//打開解碼器。
  8. AVPacket,AVFrame//獲取幀數(shù)據(jù),申請空間
  9. av_malloc(avpicture_get_size(AV_PIX_FMT_YUV420P, pCodeCtx->width, pCodeCtx->height))//緩沖區(qū)分配內(nèi)存
  10. avpicture_fill((AVPicture *)yuvFrame, out_buffer, AV_PIX_FMT_YUV420P, pCodeCtx->width, pCodeCtx->height);//初始化緩沖區(qū)
  11. while(av_read_frame(pFormatCtx,packet)//一陣一陣讀取壓縮的視頻數(shù)據(jù)AVPacket
  12. avcodec_decode_video2(pCodeCtx, frame, &got_frame, packet)//解碼AVPacket->AVFrame
  13. sws_scale//轉(zhuǎn)為指定的YUV420P像素幀
  14. fwrite(yuvFrame->data[0], 1, y_size, fp_yuv);/向YUV文件保存解碼之后的幀數(shù)據(jù)//AVFrame->YUV//一個像素包含一個Y
  15. 釋放相關(guān)內(nèi)存av_free_packet,fclose,av_frame_free,avcodec_close...

FFmpeg解碼的數(shù)據(jù)結(jié)構(gòu)(主要的namespace)

  • AVFormatContext 封裝格式上下文結(jié)構(gòu)體,也是統(tǒng)領(lǐng)全局的結(jié)構(gòu)體,保存了視頻文件封裝
    格式相關(guān)信息。

    • iformat:輸入視頻的AVInputFormat
    • nb_streams :輸入視頻的AVStream 個數(shù)
    • streams :輸入視頻的AVStream []數(shù)組
    • duration :輸入視頻的時長(以微秒為單位)
    • bit_rate :輸入視頻的碼率
  • AVInputFormat 每種封裝格式(例如FLV, MKV, MP4, AVI)對應(yīng)一個該結(jié)構(gòu)體。

    • name:封裝格式名稱
    • long_name:封裝格式的長名稱
    • extensions:封裝格式的擴(kuò)展名
    • id:封裝格式ID
    • 一些封裝格式處理的接口函數(shù)
    • FFmpeg數(shù)據(jù)結(jié)構(gòu)分析
  • AVStream 視頻文件中每個視頻(音頻)流對應(yīng)一個該結(jié)構(gòu)體。

    • id:序號
    • codec:該流對應(yīng)的AVCodecContext
    • time_base:該流的時基
    • r_frame_rate:該流的幀率
  • AVCodecContext編碼器上下文結(jié)構(gòu)體,保存了視頻(音頻)編解碼相關(guān)信息。

    • codec:編解碼器的AVCodec
    • width, height:圖像的寬高(只針對視頻)
    • pix_fmt:像素格式(只針對視頻)
    • sample_rate:采樣率(只針對音頻)
    • channels:聲道數(shù)(只針對音頻)
    • sample_fmt:采樣格式(只針對音頻)
  • AVCodec 每種視頻(音頻)編解碼器(例如H.264解碼器)對應(yīng)一個該結(jié)構(gòu)體。

    • name:編解碼器名稱
    • long_name:編解碼器長名稱
    • type:編解碼器類型
    • id:編解碼器ID
    • 一些編解碼的接口函數(shù)
  • AVPacket 存儲一幀壓縮編碼數(shù)據(jù)。

    • pts:顯示時間戳
    • dts :解碼時間戳
    • data :壓縮編碼數(shù)據(jù)
    • size :壓縮編碼數(shù)據(jù)大小
    • stream_index :所屬的AVStream
  • AVFrame存儲一幀解碼后像素(采樣)數(shù)據(jù)。

    • data:解碼后的圖像像素數(shù)據(jù)(音頻采樣數(shù)據(jù))。
    • linesize:對視頻來說是圖像中一行像素的大??;對音頻來說是音頻幀的大小。
    • width, height:圖像的寬高(只針對視頻)。
    • key_frame:是否為關(guān)鍵幀(只針對視頻) 。
    • pict_type:幀類型(只針對視頻) 。例如I,P,B。

FFmpeg解碼參照

  • 開發(fā)過程中可參考 ffmpeg-2.6.9\doc\examples中的例子.
  • 在該例子文件夾下中 有音視頻的讀寫, 過濾處理,轉(zhuǎn)碼等相關(guān)例子,大家可以進(jìn)行參考學(xué)習(xí).

FFmpeg的NDK視頻解碼播放實(shí)踐

在本例中 以視頻解碼播放為例,在下手之前,請搞懂 FFmpeg解碼的數(shù)據(jù)結(jié)構(gòu)中的命名空間相關(guān)的屬性以及作用.

1.編譯ffmpeg成so庫

在linux中編譯ffmpeg

1.編寫shell腳本文件

2.配置configuration

3.給文件權(quán)限:chmod 777 android_build.sh

4.執(zhí)行 ./android_build.sh

2.編譯libyuv庫

1.下載libyuv的庫,大家克隆即可https://chromium.googlesource.com/external/libyuv

2.執(zhí)行ndk-build(linux需要提前配置ndk環(huán)境)

3.新建工程,導(dǎo)入庫

4.配置工程環(huán)境

1.add native support.請參考 eclipse搭建NDK開發(fā)環(huán)境

2.配置Android.mk application.mk 請參考項(xiàng)目ffmpeg-palyer

3.編寫本地方法,并實(shí)現(xiàn)C代碼,生成so.

1.用自定義的SurfaceView 作為視頻渲染載體.

2.編寫本地方法,實(shí)現(xiàn)代碼.


public class CcjPlayer {
    //視頻
    public native void render(String input,Object surface);
    //音頻
    public native void sound(String input,String output);
    
    //注意加載庫的順序
    static{
        System.loadLibrary("avutil-54");
        System.loadLibrary("swresample-1");
        System.loadLibrary("avcodec-56");
        System.loadLibrary("avformat-56");
        System.loadLibrary("swscale-3");
        System.loadLibrary("postproc-53");
        System.loadLibrary("avfilter-5");
        System.loadLibrary("avdevice-56");
        System.loadLibrary("myffmpeg");
    }
}

3.生成頭文件,并實(shí)現(xiàn)方法(實(shí)現(xiàn)思路和流程請參考FFmpeg解碼流程).

/*
 * Class:     com_ccj_ffmpeg_CcjPlayer
 * Method:    render
 * Signature: (Ljava/lang/String;Ljava/lang/Object;)V
 */
JNIEXPORT void JNICALL Java_com_ccj_ffmpeg_CcjPlayer_render
(JNIEnv *env, jobject jobj, jstring input_jstr, jobject surface){
    const char* input_cstr = (*env)->GetStringUTFChars(env,input_jstr,NULL);
    //1.注冊組件
    av_register_all();

    //封裝格式上下文
    AVFormatContext *pFormatCtx = avformat_alloc_context();

    //2.打開輸入視頻文件
    if(avformat_open_input(&pFormatCtx,input_cstr,NULL,NULL) != 0){
        LOGE("%s","打開輸入視頻文件失敗");
        return;
    }
    //3.獲取視頻信息
    if(avformat_find_stream_info(pFormatCtx,NULL) < 0){
        LOGE("%s","獲取視頻信息失敗");
        return;
    }

    //視頻解碼,需要找到視頻對應(yīng)的AVStream所在pFormatCtx->streams的索引位置
    int video_stream_idx = -1;
    int i = 0;
    for(; i < pFormatCtx->nb_streams;i++){
        //根據(jù)類型判斷,是否是視頻流
        if(pFormatCtx->streams[i]->codec->codec_type == AVMEDIA_TYPE_VIDEO){
            video_stream_idx = i;
            break;
        }
    }

    //4.獲取視頻解碼器
    AVCodecContext *pCodeCtx = pFormatCtx->streams[video_stream_idx]->codec;
    AVCodec *pCodec = avcodec_find_decoder(pCodeCtx->codec_id);
    if(pCodec == NULL){
        LOGE("%s","無法解碼");
        return;
    }

    //5.打開解碼器
    if(avcodec_open2(pCodeCtx,pCodec,NULL) < 0){
        LOGE("%s","解碼器無法打開");
        return;
    }

    //編碼數(shù)據(jù)
    AVPacket *packet = (AVPacket *)av_malloc(sizeof(AVPacket));

    //像素數(shù)據(jù)(解碼數(shù)據(jù))
    AVFrame *yuv_frame = av_frame_alloc();
    AVFrame *rgb_frame = av_frame_alloc();

    /*//ScaleImg視頻縮放
    if(ScaleImg(pCodeCtx,yuv_frame,rgb_frame,pCodeCtx->width/6,pCodeCtx->width/6)!=1){
        LOGE("%s","縮放失敗");
        return;
    }*/

    //native繪制
    //窗體

    ANativeWindow* nativeWindow = ANativeWindow_fromSurface(env,surface);

    //繪制時的緩沖區(qū)
    ANativeWindow_Buffer outBuffer;

    int len ,got_frame, framecount = 0;
    //6.一陣一陣讀取壓縮的視頻數(shù)據(jù)AVPacket
    while(av_read_frame(pFormatCtx,packet) >= 0){
        //解碼AVPacket->AVFrame
        len = avcodec_decode_video2(pCodeCtx, yuv_frame, &got_frame, packet);

        //Zero if no frame could be decompressed
        //非零,正在解碼
        if(got_frame){
            LOGI("解碼%d幀",framecount++);
            //lock
            //設(shè)置緩沖區(qū)的屬性(寬、高、像素格式)
            ANativeWindow_setBuffersGeometry(nativeWindow, pCodeCtx->width, pCodeCtx->height,WINDOW_FORMAT_RGBA_8888);
            ANativeWindow_lock(nativeWindow,&outBuffer,NULL);

            //設(shè)置rgb_frame的屬性(像素格式、寬高)和緩沖區(qū)
            //rgb_frame緩沖區(qū)與outBuffer.bits是同一塊內(nèi)存
            avpicture_fill((AVPicture *)rgb_frame, outBuffer.bits, AV_PIX_FMT_RGBA, pCodeCtx->width, pCodeCtx->height);

            //YUV->RGBA_8888
            I420ToARGB(yuv_frame->data[0],yuv_frame->linesize[0],
                    yuv_frame->data[2],yuv_frame->linesize[2],
                    yuv_frame->data[1],yuv_frame->linesize[1],
                    rgb_frame->data[0], rgb_frame->linesize[0],
                    pCodeCtx->width,pCodeCtx->height);

            //unlock
            ANativeWindow_unlockAndPost(nativeWindow);

            usleep(1000 * 16);

        }

        av_free_packet(packet);
    }

    ANativeWindow_release(nativeWindow);
    av_frame_free(&yuv_frame);
    avcodec_close(pCodeCtx);
    avformat_free_context(pFormatCtx);

    (*env)->ReleaseStringUTFChars(env,input_jstr,input_cstr);
}

3.生成頭文件,并實(shí)現(xiàn)方法(實(shí)現(xiàn)思路和流程請參考FFmpeg解碼流程).

/**
 * 萬能解碼器,播放視頻器
 * @author ccj
 *
 */
public class MainActivity extends Activity {

    private CcjPlayer player;
    private VideoView videoView;
    private Spinner sp_video;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        videoView = (VideoView) findViewById(R.id.video_view);
        sp_video = (Spinner) findViewById(R.id.sp_video);
        player = new CcjPlayer();
        //選擇 <item>input.mp4</item> <item>音頻</item>
        String[] videoArray = getResources().getStringArray(R.array.video_list);
        ArrayAdapter<String> adapter = new ArrayAdapter<String>(this, 
                android.R.layout.simple_list_item_1, 
                android.R.id.text1, videoArray);
        sp_video.setAdapter(adapter);
    }

    public void mPlay(View btn){
        String video = sp_video.getSelectedItem().toString();
        if("音頻".equals(video)){
            String input = new File(Environment.getExternalStorageDirectory(),"Live.mp3").getAbsolutePath();
            String output = new File(Environment.getExternalStorageDirectory(),"Live.pcm").getAbsolutePath();
            player.sound(input, output);
            
        }else{
            String input = new File(Environment.getExternalStorageDirectory(),video).getAbsolutePath();
            //Surface傳入到Native函數(shù)中,用于繪制
            Surface surface = videoView.getHolder().getSurface();
            player.render(input, surface);
            
        }
        
    }
    
    
}

4. 導(dǎo)入input.mp4到指定位置.播放即可.

log信息


解碼器播放效果


4.不足和待改進(jìn)

1.由于繪制過程在主線程,所以會出現(xiàn)ANR,后期再處理

2.本項(xiàng)目是學(xué)習(xí)的一次實(shí)踐,后期會繼續(xù)補(bǔ)充


總結(jié)

目前市面上的音視頻解碼播放器,大多都是基于ffmpeg的代碼的,例如騰訊影音,暴風(fēng)影音等等.在此,感謝jason老師,致敬ffmpeg雷霄驊的博客.

路漫漫其修遠(yuǎn)兮,吾將上下而求索.

關(guān)于我

1.github地址https://github.com/ccj659/NDK-FFmpeg-master

2.CSDN博客http://blog.csdn.net/ccj659/

3.簡書地址http://m.itdecent.cn/users/94423b4ef5cf/latest_articles

4.QQ : 569948231

最后編輯于
?著作權(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)容