Android NDK開發之旅29 NDK FFmpeg視訊播放

小楠總發表於2017-12-21

###前言

上一篇文章我們對視訊進行了解碼,那麼這次我們隊解碼後的資料進行播放。也就是繪製到介面上。

###視訊播放

####建立自動以SurfaceView

因為視訊是需要快速實時重新整理介面的,因此要用到SurfaceView。

public class VideoView extends SurfaceView {

    public VideoView(Context context) {
        this(context, null, 0);
    }

    public VideoView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public VideoView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }

    private void init() {
        //初始化畫素繪製的格式為RGBA_8888(色彩最豐富)
        SurfaceHolder holder = getHolder();
        holder.setFormat(PixelFormat.RGBA_8888);
    }

}
複製程式碼

這裡我們自定義了一個SurfaceView,指定輸出格式為RGBA_8888,這種格式色彩豐富度最高的。

然後新增到根佈局當中:

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <com.nan.ffmpeg.view.VideoView
        android:id="@+id/sv_video"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

    <Button
        android:id="@+id/btn_play"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="播放" />

</FrameLayout>
複製程式碼

####建立播放控制器類

public class VideoPlayer {

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

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

}
複製程式碼

####native方法實現

//編碼
#include "libavcodec/avcodec.h"
//封裝格式處理
#include "libavformat/avformat.h"
//畫素處理
#include "libswscale/swscale.h"
//使用這兩個Window相關的標頭檔案需要在CMake指令碼中引入android庫
#include <android/native_window_jni.h>
#include <android/native_window.h>
#include "libyuv.h"

JNIEXPORT void JNICALL
Java_com_nan_ffmpeg_utils_VideoPlayer_render(JNIEnv *env, jobject instance, jstring input_,
                                             jobject surface) {

    //需要轉碼的視訊檔案(輸入的視訊檔案)
    const char *input = (*env)->GetStringUTFChars(env, input_, 0);

    //1.註冊所有元件,例如初始化一些全域性的變數、初始化網路等等
    av_register_all();
    //avcodec_register_all();

    //封裝格式上下文,統領全域性的結構體,儲存了視訊檔案封裝格式的相關資訊
    AVFormatContext *pFormatCtx = avformat_alloc_context();

    //2.開啟輸入視訊檔案
    if (avformat_open_input(&pFormatCtx, input, NULL, NULL) != 0) {
        LOGE("%s", "無法開啟輸入視訊檔案");
        return;
    }

    //3.獲取視訊檔案資訊,例如得到視訊的寬高
    //第二個引數是一個字典,表示你需要獲取什麼資訊,比如視訊的後設資料
    if (avformat_find_stream_info(pFormatCtx, NULL) < 0) {
        LOGE("%s", "無法獲取視訊檔案資訊");
        return;
    }

    //獲取視訊流的索引位置
    //遍歷所有型別的流(音訊流、視訊流、字幕流),找到視訊流
    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;
        }
    }

    if (v_stream_idx == -1) {
        LOGE("%s", "找不到視訊流\n");
        return;
    }

    //只有知道視訊的編碼方式,才能夠根據編碼方式去找到解碼器
    //獲取視訊流中的編解碼上下文
    AVCodecContext *pCodecCtx = pFormatCtx->streams[v_stream_idx]->codec;
    //4.根據編解碼上下文中的編碼id查詢對應的解碼器
    AVCodec *pCodec = avcodec_find_decoder(pCodecCtx->codec_id);
    //(迅雷看看,找不到解碼器,臨時下載一個解碼器,比如視訊加密了)
    if (pCodec == NULL) {
        LOGE("%s", "找不到解碼器,或者視訊已加密\n");
        return;
    }

    //5.開啟解碼器,解碼器有問題(比如說我們編譯FFmpeg的時候沒有編譯對應型別的解碼器)
    if (avcodec_open2(pCodecCtx, pCodec, NULL) < 0) {
        LOGE("%s", "解碼器無法開啟\n");
        return;
    }

    //準備讀取
    //AVPacket用於儲存一幀一幀的壓縮資料(H264)
    //緩衝區,開闢空間
    AVPacket *packet = (AVPacket *) av_malloc(sizeof(AVPacket));

    //AVFrame用於儲存解碼後的畫素資料(YUV)
    //記憶體分配
    AVFrame *yuv_frame = av_frame_alloc();
    AVFrame *rgb_frame = av_frame_alloc();

    int got_picture, ret;
    int frame_count = 0;

    //窗體
    ANativeWindow *pWindow = ANativeWindow_fromSurface(env, surface);
    //繪製時的緩衝區
    ANativeWindow_Buffer out_buffer;

    //6.一幀一幀的讀取壓縮資料
    while (av_read_frame(pFormatCtx, packet) >= 0) {
        //只要視訊壓縮資料(根據流的索引位置判斷)
        if (packet->stream_index == v_stream_idx) {
            //7.解碼一幀視訊壓縮資料,得到視訊畫素資料
            ret = avcodec_decode_video2(pCodecCtx, yuv_frame, &got_picture, packet);
            if (ret < 0) {
                LOGE("%s", "解碼錯誤");
                return;
            }

            //為0說明解碼完成,非0正在解碼
            if (got_picture) {

                //1、lock window
                //設定緩衝區的屬性:寬高、畫素格式(需要與Java層的格式一致)
                ANativeWindow_setBuffersGeometry(pWindow, pCodecCtx->width, pCodecCtx->height,
                                                 WINDOW_FORMAT_RGBA_8888);
                ANativeWindow_lock(pWindow, &out_buffer, NULL);

                //2、fix buffer

                //初始化緩衝區
                //設定屬性,畫素格式、寬高
                //rgb_frame的緩衝區就是Window的緩衝區,同一個,解鎖的時候就會進行繪製
                avpicture_fill((AVPicture *) rgb_frame, out_buffer.bits, AV_PIX_FMT_RGBA,
                               pCodecCtx->width,
                               pCodecCtx->height);

                //YUV格式的資料轉換成RGBA 8888格式的資料
                //FFmpeg可以轉,但是會有問題,因此我們使用libyuv這個庫來做
                //https://chromium.googlesource.com/external/libyuv
                //引數分別是資料、對應一行的大小
                //I420ToARGB(yuv_frame->data[0], yuv_frame->linesize[0],
                //           yuv_frame->data[1], yuv_frame->linesize[1],
                //           yuv_frame->data[2], yuv_frame->linesize[2],
                //           rgb_frame->data[0], rgb_frame->linesize[0],
                //           pCodecCtx->width, pCodecCtx->height);

                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],
                           pCodecCtx->width, pCodecCtx->height);

                //3、unlock window
                ANativeWindow_unlockAndPost(pWindow);

                frame_count++;
                LOGI("解碼繪製第%d幀", frame_count);
            }
        }

        //釋放資源
        av_free_packet(packet);
    }

    av_frame_free(&yuv_frame);
    avcodec_close(pCodecCtx);
    avformat_free_context(pFormatCtx);
    (*env)->ReleaseStringUTFChars(env, input_, input);
}
複製程式碼

程式碼裡面需要注意的是,我們使用了yuvlib這個庫對YUV資料轉換為RGB資料。這個庫的下載地址是https://chromium.googlesource.com/external/libyuv。

修改Android.mk檔案最後一行,由輸出靜態庫改為輸出動態庫:

include $(BUILD_SHARED_LIBRARY)
複製程式碼

自己新建jni目錄,把所有檔案拷貝到裡面,Linux執行下面的命令編譯yuvlib:

ndk-build jni
複製程式碼

然後就會輸出預編譯的so庫,並且在Android Studio中使用了。CMake指令碼需要新增:

#YUV轉RGB需要的庫
add_library( yuv
             SHARED
             IMPORTED
            )

set_target_properties(
                       yuv
                       PROPERTIES IMPORTED_LOCATION
                       ${path_project}/app/libs/${ANDROID_ABI}/libyuv.so
                    )
複製程式碼

如果需要的話設定一些標頭檔案的包含路徑:

#配置標頭檔案的包含路徑
include_directories(${path_project}/app/src/main/cpp/include/yuv)
複製程式碼

最後在使用的時候包含對應的標頭檔案即可:

#include "libyuv.h"
複製程式碼

還有一個注意的地方就是我們要使用到視窗的原生繪製,那麼就需要引入window相關的標頭檔案:

//使用這兩個Window相關的標頭檔案需要在CMake指令碼中引入android庫
#include <android/native_window_jni.h>
#include <android/native_window.h>
複製程式碼

這些標頭檔案使用需要把android這個庫編譯進來,使用方法跟log庫一樣:

#找到Android的Window繪製相關的庫(這個庫已經在Android平臺中了)
find_library(
            android-lib
            android
            )
複製程式碼

記得連結到自己的庫裡面:

#把需要的庫連結到自己的庫中
target_link_libraries(
            ffmpeg-lib
            ${log-lib}
            ${android-lib}
            avutil
            swresample
            avcodec
            avformat
            swscale
            postproc
            avfilter
            avdevice
            yuv
            )
複製程式碼

####視窗的原生繪製流程

繪製需要一個surface物件。

原生繪製步驟:

  1. lock Window。
  2. 初始化緩衝區,設定大小,緩衝區賦值。
  3. 解鎖然後就繪製到視窗中了。

####測試

@Override
public void onClick(View v) {
    String inputVideo = Environment.getExternalStorageDirectory().getAbsolutePath() + File.separatorChar
            + "input.mp4";

    switch (v.getId()) {

        case R.id.btn_play:
			sv_video = (SurfaceView) findViewById(R.id.sv_video);
            //native方法中傳入SurfaceView的Surface物件,在底層進行繪製
            mPlayer.render(inputVideo, sv_video.getHolder().getSurface());
            break;

    }
}
複製程式碼

如果覺得我的文字對你有所幫助的話,歡迎關注我的公眾號:

公眾號:Android開發進階

我的群歡迎大家進來探討各種技術與非技術的話題,有興趣的朋友們加我私人微信huannan88,我拉你進群交(♂)流(♀)

相關文章