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压缩视频