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中,太杂了。

运行效果:
《FFmpeg(五):JNI动态注册方法调用FFmpeg播放视频》

结语:

到此整个配置基本完成,由于主界面的播放我用了RxJava和多媒体选择后播放,代码量有点多,这里就不贴出,具体实现可看项目源码。

参考文章
对于JNI方法名,数据类型和方法签名的一些认识
Android深入理解JNI(一)JNI原理与静态、动态注册
初识JNI(二)-静态注册和动态注册

下一章讲:
FFmpeg(六):使用FFmpeg压缩视频

github源码

简书半停更说明

碎碎念:如果诸君喜欢,请点个赞
更多问题,欢迎加群:584275290
    原文作者:牟仯
    原文地址: https://www.jianshu.com/p/52699b383da7
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞