Android NDK開發(fā)之旅33--FFmpeg視頻播放

Android NDK開發(fā)之旅 目錄

1.播放多媒體文件步驟

通常情況下,我們下載的視頻文件如MP4,MKV、FLV等都屬于封裝格式,就是把音視頻數(shù)據(jù)按照相應(yīng)的規(guī)范,打包成一個文本文件。我們可以使用MediaInfo這個工具查看媒體文件的相關(guān)信息。
當(dāng)我們播放一個媒體文件時,通常需要經(jīng)過以下幾個步驟如下:


音視頻解碼步驟
  • 解封裝(Demuxing):就是將輸入的封裝格式的數(shù)據(jù),分離成為音頻流壓縮編碼數(shù)據(jù)和視頻流壓縮編碼數(shù)據(jù)。封裝格式種類很多,例如MP4,MKV,RMVB,TS,F(xiàn)LV,AVI等等,它的作用就是將已經(jīng)壓縮編碼的視頻數(shù)據(jù)和音頻數(shù)據(jù)按照一定的格式放到一起。例如,F(xiàn)LV格式的數(shù)據(jù),經(jīng)過解封裝操作后,輸出H.264編碼的視頻碼流和AAC編碼的音頻碼流。

  • 解碼(Decode):就是將視頻/音頻壓縮編碼數(shù)據(jù),解碼成為非壓縮的視頻/音頻原始數(shù)據(jù)。音頻的壓縮編碼標(biāo)準(zhǔn)包含AAC,MP3等,視頻的壓縮編碼標(biāo)準(zhǔn)則包含H.264,MPEG2等。解碼是整個系統(tǒng)中最重要也是最復(fù)雜的一個環(huán)節(jié)。通過解碼,壓縮編碼的視頻數(shù)據(jù)輸出成為非壓縮的顏色數(shù)據(jù),例如YUV、RGB等等;壓縮編碼的音頻數(shù)據(jù)輸出成為非壓縮的音頻抽樣數(shù)據(jù),例如PCM數(shù)據(jù)。

  • 音視頻同步:就是根據(jù)解封裝模塊處理過程中獲取到的參數(shù)信息,同步解碼出來的音頻和視頻數(shù)據(jù),并將音視頻頻數(shù)據(jù)送至系統(tǒng)的顯卡和聲卡播放出來(Render)。

2.FFmpeg介紹

Android需要音/視頻編解碼需要用到FFmpeg的so庫,請查看

Android NDK開發(fā)之旅29--云服務(wù)器Ubuntu下搭建NDK環(huán)境,并編譯FFmpeg

FFmpeg是一套可以用來記錄、轉(zhuǎn)換數(shù)字音頻、視頻,并能將其轉(zhuǎn)化為流的開源計算機程序。它包括了領(lǐng)先的音/視頻編解碼庫libavcodec、libavformat、libswscale等。

首先介紹一下ffmpeg里面各模塊的功能把:

庫名 工具
libavformat 用于各種音視頻封裝格式的生成和解析,包括獲取解碼所需信息以生成解碼上下文結(jié)構(gòu)和讀取音視頻幀等功能;音視頻的格式解析協(xié)議,為libavcodec分析碼流提供獨立的音頻或視頻碼流源。
libavcodec 用于各種類型聲音/圖像編解碼;該庫是音視頻編解碼核心,實現(xiàn)了市面上可見的絕大部分解碼器的功能,libavcodec庫被其他各大解碼器ffdshow,MPlayer等所包含或應(yīng)用。
libavdevice 硬件采集、加速、顯示。操作計算機中常用的音視頻捕獲或輸出設(shè)備;
libavfilter filter音視頻濾波器的開發(fā),如寬高比、剪裁、格式化、非格式化、伸縮。
libavutil 包含一些公共的工具函數(shù)的使用庫,包括算數(shù)運算、字符操作。
libavresample 音視頻封裝編解碼格式預(yù)設(shè)等。
libswscale (原始視頻格式轉(zhuǎn)換)用于視頻場景比例縮放、色彩映射轉(zhuǎn)換;圖像顏色空間或格式轉(zhuǎn)換。
libswresample 原始音頻格式轉(zhuǎn)碼
libpostproc (同步、時間計算的簡單算法)用于后期效果處理;音視頻應(yīng)用的后期處理,如圖像的去塊效應(yīng)。

3.FFmpeg音視頻解碼過程

通過上面對媒體文件播放步驟的了解,我們在解碼多媒體文件的時候需要經(jīng)過兩個步驟,即解封裝(Demuxing)和解碼(Decode)。下面就來看一下FFMPEG解碼媒體文件的時候是怎樣做這兩個步驟的。


3.1.注冊所有組件

av_register_all();

這個函數(shù),可以注冊所有支持的容器和對應(yīng)的codec。

3.2.打開輸入視頻文件

AVFormatContext *pFormatCtx = avformat_alloc_context();
avformat_open_input(&pFormatCtx,input_cstr,NULL,NULL);

3.3.獲取視頻文件信息

    avformat_find_stream_info(pFormatCtx,NULL);
    //獲取視頻流的索引位置
    //遍歷所有類型的流(音頻流、視頻流、字幕流),找到視頻流
    int v_stream_idx = -1;
    int i = 0;
    //number of streams
    for (; i < pFormatCtx->nb_streams; i++)
    {
        //流的類型
        if (pFormatCtx->streams[i]->codec->codec_type == AVMEDIA_TYPE_VIDEO)
        {
            v_stream_idx = i;
            break;
        }
    }

3.4.根據(jù)編解碼上下文中的編碼id查找對應(yīng)的解碼器

    AVCodecContext *pCodecCtx = pFormatCtx->streams[v_stream_idx]->codec;
    AVCodec *pCodec = avcodec_find_decoder(pCodecCtx->codec_id);

3.5.打開解碼器

avcodec_open2(pCodecCtx,pCodec,NULL)

來打開解碼器,AVFormatContext、AVStream、AVCodecContext、AVCodec四者之間的關(guān)系為


3.6.一幀一幀讀取壓縮的視頻數(shù)據(jù)AVPacket

while (av_read_frame(pFormatCtx, packet) >= 0) {
省略...
}

3.7.解碼一幀視頻壓縮數(shù)據(jù),得到視頻像素數(shù)據(jù)

avcodec_decode_video2(pCodecCtx, pFrame, &got_picture, packet)

4.Android整體項目下視頻播放流程

4.1輸入視頻路徑

4.2.把視頻數(shù)據(jù)解碼為YUV像素數(shù)據(jù)

4.3.YUV數(shù)據(jù)轉(zhuǎn)化為RGB格式。 (這一步可以省略)

4.4.一幀一幀的傳給SurfaceView顯示出來

注意:

其實YUV數(shù)據(jù)可直接在SurfaceView顯示,在研究Android系統(tǒng)多媒體框架的stagefright視頻顯示時發(fā)現(xiàn),根本找不到omx解碼后的yuv是怎么轉(zhuǎn)換成RGB的代碼,yuv數(shù)據(jù)在render之后就找不到去向了,可畫面確確實實的顯示出來了。

稍微看一下AsomePlayer的代碼,不難發(fā)現(xiàn),視頻的每一幀是通過調(diào)用了SoftwareRenderer來渲染顯示的、這是一個很大的突破,以后可以直接丟yuv數(shù)據(jù)到surface顯示,無需耗時耗效率的yuv轉(zhuǎn)RGB了,這部分知識點會在以后的文章中實現(xiàn)本篇不涉及。

5.關(guān)鍵代碼

VideoUtils.class
package com.haocai.ffmpegtest;

import android.view.Surface;

public class VideoUtils {

    public native void render(String input,Surface surface);

    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");
    }
}

activity_simple_play.xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <SurfaceView
        android:id="@+id/video_view"
        android:layout_width="fill_parent"
        android:layout_height="fill_parent"/>

</LinearLayout>
SimplePlayActivity.class
public class SimplePlayActivity extends Activity  implements SurfaceHolder.Callback {


    @BindView(R.id.video_view)
    SurfaceView videoView;
    private VideoUtils player;
    SurfaceHolder surfaceHolder;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_simple_play);
        ButterKnife.bind(this);
        player = new VideoUtils();
        surfaceHolder = videoView.getHolder();
        //surface
        surfaceHolder.addCallback(this);
    }

    @Override
    public void surfaceCreated(final SurfaceHolder holder) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                String input = new File(Environment.getExternalStorageDirectory(),"小蘋果.mp4").getAbsolutePath();
                Log.d("main",input);
                player.render(input, holder.getSurface());
            }
        }).start();
    }

    @Override
    public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {

    }

    @Override
    public void surfaceDestroyed(SurfaceHolder holder) {
        holder.getSurface().release();
    }
}
ffmpeg_player.c
#include <com_haocai_ffmpegtest_VideoUtils.h>
#include <android/log.h>
#include <android/native_window_jni.h>
#include <android/native_window.h>
#include <stdio.h>
//解碼
#include "include/libavcodec/avcodec.h"
//封裝格式處理
#include "include/libavformat/avformat.h"
//像素處理
#include "include/libswscale/swscale.h"

#define  LOG_TAG    "ffmpegandroidplayer"
#define  LOGI(FORMAT,...) __android_log_print(ANDROID_LOG_INFO,LOG_TAG,FORMAT,##__VA_ARGS__);
#define  LOGE(FORMAT,...) __android_log_print(ANDROID_LOG_ERROR,LOG_TAG,FORMAT,##__VA_ARGS__);
#define  LOGD(FORMAT,...)  __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG,FORMAT, ##__VA_ARGS__)

JNIEXPORT void JNICALL Java_com_haocai_ffmpegtest_VideoUtils_render
  (JNIEnv *env, jobject jobj, jstring input_jstr, jobject surface){
    const char* file_name = (*env)->GetStringUTFChars(env, input_jstr, NULL);


    LOGD("play");


    av_register_all();

    AVFormatContext *pFormatCtx = avformat_alloc_context();

    // Open video file
    if (avformat_open_input(&pFormatCtx, file_name, NULL, NULL) != 0) {

        LOGD("Couldn't open file:%s\n", file_name);
        return ; // Couldn't open file
    }

    // Retrieve stream information
    if (avformat_find_stream_info(pFormatCtx, NULL) < 0) {
        LOGD("Couldn't find stream information.");
        return ;
    }

    // Find the first video stream
    int videoStream = -1, i;
    for (i = 0; i < pFormatCtx->nb_streams; i++) {
        if (pFormatCtx->streams[i]->codec->codec_type == AVMEDIA_TYPE_VIDEO
            && videoStream < 0) {
            videoStream = i;
        }
    }
    if (videoStream == -1) {
        LOGD("Didn't find a video stream.");
        return ; // Didn't find a video stream
    }

    // Get a pointer to the codec context for the video stream
    AVCodecContext *pCodecCtx = pFormatCtx->streams[videoStream]->codec;

    // Find the decoder for the video stream
    AVCodec *pCodec = avcodec_find_decoder(pCodecCtx->codec_id);
    if (pCodec == NULL) {
        LOGD("Codec not found.");
        return ; // Codec not found
    }

    if (avcodec_open2(pCodecCtx, pCodec, NULL) < 0) {
        LOGD("Could not open codec.");
        return ; // Could not open codec
    }

    // 獲取native window
    ANativeWindow *nativeWindow = ANativeWindow_fromSurface(env, surface);

    // 獲取視頻寬高
    int videoWidth = pCodecCtx->width;
    int videoHeight = pCodecCtx->height;

    // 設(shè)置native window的buffer大小,可自動拉伸
    ANativeWindow_setBuffersGeometry(nativeWindow, videoWidth, videoHeight,
                                     WINDOW_FORMAT_RGBA_8888);
    ANativeWindow_Buffer windowBuffer;

    if (avcodec_open2(pCodecCtx, pCodec, NULL) < 0) {
        LOGD("Could not open codec.");
        return ; // Could not open codec
    }

    // Allocate video frame
    AVFrame *pFrame = av_frame_alloc();

    // 用于渲染
    AVFrame *pFrameRGBA = av_frame_alloc();
    if (pFrameRGBA == NULL || pFrame == NULL) {
        LOGD("Could not allocate video frame.");
        return ;
    }

    // Determine required buffer size and allocate buffer
    // buffer中數(shù)據(jù)就是用于渲染的,且格式為RGBA
    int numBytes = av_image_get_buffer_size(AV_PIX_FMT_RGBA, pCodecCtx->width, pCodecCtx->height,
                                            1);
    uint8_t *buffer = (uint8_t *) av_malloc(numBytes * sizeof(uint8_t));
    av_image_fill_arrays(pFrameRGBA->data, pFrameRGBA->linesize, buffer, AV_PIX_FMT_RGBA,
                         pCodecCtx->width, pCodecCtx->height, 1);

    // 由于解碼出來的幀格式不是RGBA的,在渲染之前需要進行格式轉(zhuǎn)換
    struct SwsContext *sws_ctx = sws_getContext(pCodecCtx->width,
                                                pCodecCtx->height,
                                                pCodecCtx->pix_fmt,
                                                pCodecCtx->width,
                                                pCodecCtx->height,
                                                AV_PIX_FMT_RGBA,
                                                SWS_BILINEAR,
                                                NULL,
                                                NULL,
                                                NULL);

    int frameFinished;
    AVPacket packet;
    while (av_read_frame(pFormatCtx, &packet) >= 0) {
        // Is this a packet from the video stream?
        if (packet.stream_index == videoStream) {

            // Decode video frame
            avcodec_decode_video2(pCodecCtx, pFrame, &frameFinished, &packet);

            // 并不是decode一次就可解碼出一幀
            if (frameFinished) {

                // lock native window buffer
                ANativeWindow_lock(nativeWindow, &windowBuffer, 0);

                // 格式轉(zhuǎn)換
                sws_scale(sws_ctx, (uint8_t const *const *) pFrame->data,
                          pFrame->linesize, 0, pCodecCtx->height,
                          pFrameRGBA->data, pFrameRGBA->linesize);

                // 獲取stride
                uint8_t *dst = (uint8_t *) windowBuffer.bits;
                int dstStride = windowBuffer.stride * 4;
                uint8_t *src = (pFrameRGBA->data[0]);
                int srcStride = pFrameRGBA->linesize[0];

                // 由于window的stride和幀的stride不同,因此需要逐行復(fù)制
                int h;
                for (h = 0; h < videoHeight; h++) {
                    memcpy(dst + h * dstStride, src + h * srcStride, srcStride);
                }

                ANativeWindow_unlockAndPost(nativeWindow);
                //延時繪制 否則視頻快速播放
                usleep(1000 * 16);
            }

        }
        av_packet_unref(&packet);
    }

    av_free(buffer);
    av_free(pFrameRGBA);

    // Free the YUV frame
    av_free(pFrame);

    // Close the codecs
    avcodec_close(pCodecCtx);

    // Close the video file
    avformat_close_input(&pFormatCtx);
    (*env)->ReleaseStringUTFChars(env, input_jstr, file_name);
    return ;
}
說明:其它視頻格式也支持

6.播放效果

源碼下載

Github:https://github.com/kpioneer123/FFmpegTest

特別感謝:

CrazyDiode
小碼哥_WS






微信號kpioneer

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。