FFmpeg(五):JNI動態注冊方法調用FFmpeg播放視頻

前言

這篇文章講如何用JNI動態注冊的方法調用FFmpeg播放視頻。FFmpeg播放視頻網上的教程很多,而且都講的很好,所以這篇文章講的更多的是如何改造native-lib.cpp來實現動態注冊方法。

正文

1 靜態注冊和動態注冊

在上篇文章中我們實現了FFmpeg相關信息的打印,JNI調用的方法使用的靜態注冊:

JNIEXPORT jstring JNICALL Java_com_pvirtech_ffmpeg4android_FFmpegKit_stringFromFFmpeg(···)

方法名必須這樣,夠長吧,還必須得這種格式。關于JNI的靜態注冊和動態注冊盆友們可以看看這些大佬文章,我這里就不重述:
Android深入理解JNI(一)JNI原理與靜態、動態注冊
初識JNI(二)-靜態注冊和動態注冊

兩者相比,靜態方法注冊的缺點:

1.必須遵循某些規則,名字過長
2.多個class需Javah多遍,
3.用到時才尋找并加載,效率低

動態注冊優點

注冊在JNI層實現的,JAVA層不需要關心,因為在system.load時就會去調JNI_OnLoad有選擇性的注冊。

當然,這個優缺點也是大佬們的總結,我并沒有深入到JVM虛擬機中去求證這個總結的真實性。但我對于動態注冊的總結就是:真TM好用

2 操刀代碼

  • 2.1FFmpegKit中定義play()方法
public class FFmpegKit {
 ...
public static native String stringFromFFmpeg();

public native static int play(SurfaceView surface,String url);
}
  • 2.2 native-lib.cpp中動態注冊play()方法(其他方法也改為動態注冊)
    • 定義static方法nativeStringFromFFmpeg(JNIEnv *env,jobject obj)方法

      static jstring nativeStringFromFFmpeg(JNIEnv *env, jobject obj) {
       char info[10000] = {0};
       sprintf(info, "%s\n", avcodec_configuration());
       return env->NewStringUTF(info);
      }
      
    • 定義static方法nativePlay(JNIEnv *env,jobject obj,jobject surface,jstring url)

      static jint nativePlay(JNIEnv *env,jobject obj,jobject surface,jstring url) {
          // sd卡中的視頻文件地址,可自行修改或者通過jni傳入
          //char *file_name = "/storage/emulated/0/DCIM/Camera/123.mp4";
          char *file_url = (char *)env->GetStringUTFChars(url,0);
          av_register_all();
      
          AVFormatContext *pFormatCtx = avformat_alloc_context();
      
          // Open video file
          if (avformat_open_input(&pFormatCtx, file_url , NULL, NULL) != 0) {
      
              LOGD("Couldn't open file:%s\n", file_url );
       return -1; // Couldn't open file
          }
      
          // Retrieve stream information
          if (avformat_find_stream_info(pFormatCtx, NULL) < 0) {
              LOGD("Couldn't find stream information.");
              return -1;
          }
      
          // 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 -1; // 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 -1; // Codec not found
          }
      
          if (avcodec_open2(pCodecCtx, pCodec, NULL) < 0) {
              LOGD("Could not open codec.");
              return -1; // Could not open codec
          }
      
          // 獲取native window
          ANativeWindow *nativeWindow = ANativeWindow_fromSurface(env, surface);
      
          // 獲取視頻寬高
          int videoWidth = pCodecCtx->width;
          int videoHeight = pCodecCtx->height;
      
          // 設置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 -1; // 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 -1;
          }
      
          // Determine required buffer size and allocate buffer
          // buffer中數據就是用于渲染的,且格式為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的,在渲染之前需要進行格式轉換
          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);
      
           // 格式轉換
           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不同,因此需要逐行復制
           int h;
           for (h = 0; h < videoHeight; h++) {
               memcpy(dst + h * dstStride, src + h * srcStride, srcStride);
           }
      
           ANativeWindow_unlockAndPost(nativeWindow);
                  }
      
              }
              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);
          return 0;
      }
      
    • 申明方法數組:

      JNINativeMethod nativeMethod[] = {
      {"play",             "(Ljava/lang/Object;)I",  (void *) nativePlay},
      {"stringFromFFmpeg", "()Ljava/lang/String;",   (void *) nativeStringFromFFmpeg}    };
      

      觀察可以看到每一項中有三個參數,我們以第一項為例子,第一個參數play指的是FFmpegKit中的play (Object surface)方法;第二個參數為JNI驗證簽名,定義傳入什么類型的參數和返回返回什么值的類型,如(Ljava/lang/Object;)I,仔細觀察這個是按照括號分"(內值)外值",括號內指的是傳入那些類型參數,括號外是指返回值的類型;第三個參數指的是動態方法nativePlay,實際的運行效果就是當Androd調用FFmpegKit中的play(Object surface)方法時,就會去調用動態方法nativePlay,傳入什么參數和返回什么類型都通過JNI簽名規則申明好了。關于第二個參數JNI驗證簽名,可以在文末參考相關文章鏈接。

    • 重寫JNI_OnLoad方法:

       JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *jvm, void *reserved) {JNIEnv *env;
       if (jvm->GetEnv((void **) &env, JNI_VERSION_1_4) != JNI_OK) {
              return -1;
       }
       // 看看FFmpegKit中的方法有多少在方法數組中申明了,有選擇性的加載
       jclass clz = env->FindClass("com/pvirtech/ffmpeg4android/utils/FFmpegKit");
       env->RegisterNatives(clz, nativeMethod, sizeof(nativeMethod) / sizeof(nativeMethod[0]));
      
       return JNI_VERSION_1_4;
       }
      

3 注意三點

  • 1 native-lib.cpp中有個extern "C"{...},如果是靜態注冊,大括號要把頭文件和靜態方法全部括起來,如果是動態注冊,大括號只需要括頭文件申明,動態方法不能括,否則報錯,我也不清楚為什么,如示:
    靜態注冊:

     extern "C" {
     //以下是頭文件申明
    #include "libswresample/swresample.h"
     //以上是頭文件申明
          JNIEXPORT jstring JNICALL Java_com_pvirtech_ffmpeg4android_FFmpegKit_stringFromFFmpeg
             (JNIEnv *env, jobject obj) {...}
    
          JNIEXPORT jstring JNICALL Java_com_pvirtech_ffmpeg4android_FFmpegKit_play
             (JNIEnv *env, jobject obj,jobject surface,jstring url) {...}
     };//大括號要把所有的方法括起來
    

    動態注冊

     extern "C" {
     #include "libavcodec/avcodec.h"
     ...
     };//大括號到此位置,只擴頭文件
    
     static jstring play(JNIEnv *env, jobject obj,jobject surface,jstring url) {
     ...
     }
     JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *jvm, void *reserved) {
     ...
     }
     JNINativeMethod nativeMethod[]={...}
    
  • native-lib.cpp的方法按照 extern "C"->各個動態方法->nativeMethod方法數組申明->JNI_OnLoad由上到下排序,否則預加載沒有會報錯

  • FFmpeg的視頻播放方向有問題
    *后續有可能把播放視頻、壓縮視頻、打印相關信息等方法單獨提出來,不全部放到native-lib.cpp中,太雜了。

運行效果:

結語:

到此整個配置基本完成,由于主界面的播放我用了RxJava和多媒體選擇后播放,代碼量有點多,這里就不貼出,具體實現可看項目源碼。

參考文章
對于JNI方法名,數據類型和方法簽名的一些認識
Android深入理解JNI(一)JNI原理與靜態、動態注冊
初識JNI(二)-靜態注冊和動態注冊

下一章講:
FFmpeg(六):使用FFmpeg壓縮視頻

github源碼

簡書半停更說明

碎碎念:如果諸君喜歡,請點個贊
更多問題,歡迎加群:584275290
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 什么是JNI? JNI 是java本地開發接口.JNI 是一個協議,這個協議用來溝通java代碼和外部的本地代碼(...
    a_tomcat閱讀 2,845評論 0 54
  • //音頻編碼 JNIEXPORT void JNICALL Java_com_tz_dream_ffmpeg_an...
    Jackey_song閱讀 1,557評論 0 1
  • 教程一:視頻截圖(Tutorial 01: Making Screencaps) 首先我們需要了解視頻文件的一些基...
    90后的思維閱讀 4,749評論 0 3
  • #include #include #include #include #include #include #in...
    lichao3140閱讀 1,138評論 0 0
  • 今天汗水是最大的付出,收獲的是 第一,一個團隊只能有一個領導 第二,整個團隊需要整齊一致的目標導向 第三,團隊間要...
    可愛的_414d閱讀 456評論 4 5