前言
這篇文章講如何用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壓縮視頻