1. 程式人生 > >LibVLC for android 解碼視訊並獲取每一幀

LibVLC for android 解碼視訊並獲取每一幀

一、背景

      最近有一個需求,使用android系統的裝置,從IP攝像頭(RTSP SERVER)獲取到的視訊中的每一幀進行處理(人臉檢測),直接使用ffmpeg進行實現比較簡單,但是苦於對ffmpeg不太熟悉,獲取到的視訊延遲較高,只好轉戰看看LibVLC能否獲得更好的效果。

        兩篇文章幫助較大,在此感謝:

二、主要思路

     主要思路是利用libVLC 現有的API:libvlc_video_set_format()  、libvlc_video_set_callbacks() 對libvlc解碼後的幀進行擷取,然後把設定callback的介面通過JNI暴露給JAVA層。這兩個函式都在libvlc_media_player.h中有具體的說明。下面分別進行分析。

三、 具體實現

3.1 Lib層

3.1.1 libvlc_video_set_format() :
/**
 * Set decoded video chroma and dimensions.
 * This only works in combination with libvlc_video_set_callbacks(),
 * and is mutually exclusive with libvlc_video_set_format_callbacks().
 *
 * \param mp the media player
 * \param chroma a four-characters string identifying the chroma
 *               (e.g. "RV32" or "YUYV")
 * \param width pixel width
 * \param height pixel height
 * \param pitch line pitch (in bytes)
 * \version LibVLC 1.1.1 or later
 * \bug All pixel planes are expected to have the same pitch.
 * To use the YCbCr color space with chrominance subsampling,
 * consider using libvlc_video_set_format_callbacks() instead.
 */
LIBVLC_API
void libvlc_video_set_format( libvlc_media_player_t *mp, const char *chroma,
                              unsigned width, unsigned height,
                              unsigned pitch );

該函式的作用是設定你希望的輸出幀的格式和尺寸,libvlc會非常賣力地幫你解碼,然後轉換成你希望的格式。

· chroma為視訊輸出幀格式的選擇,可以是常用的YUYV、NV12、RGBA等各種格式;

· width、height為你希望輸出的幀的長寬;

· pitch 直接的翻譯是“場”,它的值是 幀寬度 × 每個畫素所佔的位元組數,當然每個畫素的位元組數是由你選擇的幀格式決定的,比如RGBA一個畫素4個位元組,YUYV一個畫素1.5個位元組。具體自己問google。

3.1.2 libvlc_video_set_callbacks():
/**
 * Set callbacks and private data to render decoded video to a custom area
 * in memory.
 * Use libvlc_video_set_format() or libvlc_video_set_format_callbacks()
 * to configure the decoded format.
 *
 * \warning Rendering video into custom memory buffers is considerably less
 * efficient than rendering in a custom window as normal.
 *
 * For optimal perfomances, VLC media player renders into a custom window, and
 * does not use this function and associated callbacks. It is <b>highly
 * recommended</b> that other LibVLC-based application do likewise.
 * To embed video in a window, use libvlc_media_player_set_xid() or equivalent
 * depending on the operating system.
 *
 * If window embedding does not fit the application use case, then a custom
 * LibVLC video output display plugin is required to maintain optimal video
 * rendering performances.
 *
 * The following limitations affect performance:
 * - Hardware video decoding acceleration will either be disabled completely,
 *   or require (relatively slow) copy from video/DSP memory to main memory.
 * - Sub-pictures (subtitles, on-screen display, etc.) must be blent into the
 *   main picture by the CPU instead of the GPU.
 * - Depending on the video format, pixel format conversion, picture scaling,
 *   cropping and/or picture re-orientation, must be performed by the CPU
 *   instead of the GPU.
 * - Memory copying is required between LibVLC reference picture buffers and
 *   application buffers (between lock and unlock callbacks).
 *
 * \param mp the media player
 * \param lock callback to lock video memory (must not be NULL)
 * \param unlock callback to unlock video memory (or NULL if not needed)
 * \param display callback to display video (or NULL if not needed)
 * \param opaque private pointer for the three callbacks (as first parameter)
 * \version LibVLC 1.1.1 or later
 */
LIBVLC_API
void libvlc_video_set_callbacks( libvlc_media_player_t *mp,
                                 libvlc_video_lock_cb lock,
                                 libvlc_video_unlock_cb unlock,
                                 libvlc_video_display_cb display,
                                 void *opaque );

這個函式是用來設定在libvlc解碼過程中的三個回撥的,觸發的時機分別為:

·libvlc_video_lock_cb - 有一個新的幀要開始解碼;

·libvlc_video_unlock_cb - 當前幀解碼完成;

·libvlc_video_display_cb - 開始顯示。

PS:需要注意的是,在我們自己呼叫這些這個函式設定這些回撥時,其他流程中設定的回撥就會失效。比如在java中呼叫ivlcVout.setVideoView(textureView)設定輸出的View時,實際上是會對這三個回撥進行設定的,而在這之後手動呼叫 libvlc_video_set_callbacks(),就會對這些回撥進行覆蓋,使之不在會在MediaPlayer.play時,顯示在View中。如果還需要顯示在View中,則需要進行其他的處理。

3.2 JNI層

void
Java_org_videolan_libvlc_MediaPlayer_nativeSetVideoFormat(JNIEnv *env, jobject thiz, 
   jstring format, jint width, jint height, jint pitch) {

   vlcjni_object *p_obj = VLCJniObject_getInstance(env, thiz);

   if (!p_obj)
       return;

   const char *formatStr = (*env)->GetStringUTFChars(env, format, NULL);

   libvlc_video_set_format(p_obj->u.p_mp, formatStr, width, height, pitch);

   (*env)->ReleaseStringUTFChars(env, format, formatStr);
}

struct myfield {
   jclass mediaPlayerClazz;
   jmethodID onDisplayCallback;
   jobject thiz;
   void *buffer;
} myfield;

static void *lock(void *data, void ** p_pixels) {
   *p_pixels = myfield.buffer;
   return NULL;
}

static void unlock(void *data, void *id, void * const * p_pixels) {
}

static pthread_mutex_t myMutex = PTHREAD_MUTEX_INITIALIZER;

static void display(void *data, void *id) {
   JavaVM *jvm = fields.jvm; //這個地方需要修改 "utils.h" 中的struct fields,增加成員JavaVM* jvm,並且在libvlcjni.c的中進行賦值
   JNIEnv *env; 
   int stat = (*jvm)->GetEnv(jvm, (void **)&env, JNI_VERSION_1_2);
   if (stat == JNI_EDETACHED) {
       if ((*jvm)->AttachCurrentThread(jvm, (void **) &env, NULL) != 0) {
           return;
       }
   } else if (stat == JNI_OK) {
       //
   } else if (stat == JNI_EVERSION) {
       return;
   }

   pthread_mutex_lock(&myMutex);

   if (myfield.thiz != NULL) {
       (*env)->CallVoidMethod(env, myfield.thiz, myfield.onDisplayCallback);
   }

   pthread_mutex_unlock(&myMutex);

   (*jvm)->DetachCurrentThread(jvm);
}

void
Java_org_videolan_libvlc_MediaPlayer_nativeSetVideoBuffer(JNIEnv *env, jobject thiz, jobject buffer) {
   vlcjni_object *p_obj = VLCJniObject_getInstance(env, thiz);

   libvlc_media_player_t *mp = p_obj->u.p_mp;
   if (!mp) {
       return;
   }

   if (buffer == NULL) {
       (*env)->DeleteGlobalRef(env, myfield.mediaPlayerClazz);
       pthread_mutex_lock(&myMutex);
       (*env)->DeleteGlobalRef(env, myfield.thiz);
       myfield.thiz = NULL;
       pthread_mutex_unlock(&myMutex);
       return;
   }

   myfield.mediaPlayerClazz = (*env)->FindClass(env, "org/videolan/libvlc/MediaPlayer");
   myfield.mediaPlayerClazz = (jclass) (*env)->NewGlobalRef(env, myfield.mediaPlayerClazz);
   myfield.onDisplayCallback = (*env)->GetMethodID(env, myfield.mediaPlayerClazz, "onDisplay", "()V");
   myfield.thiz = (*env)->NewGlobalRef(env, thiz);
   myfield.buffer = (*env)->GetDirectBufferAddress(env, buffer);

   libvlc_video_set_callbacks(mp, lock, NULL, display, NULL);
}

在libvlcjni.c的中對fields.jvm進行賦值

int VLCJNI_OnLoad(JavaVM *vm, JNIEnv* env)
{
    myVm = vm;
    fields.jvm = vm;
    /* Create a TSD area and setup a destroy callback when a thread that
     * previously set the jni_env_key is canceled or exited */
    if (pthread_key_create(&jni_env_key, jni_detach_thread) != 0)
        return -1;
    ...
}

3.3 JAVA層

2.3.1 修改org/videolan/libvlc/MediaPlayer.java

增加JNI介面:

    private native void nativeSetVideoFormat(String format, int width, int height, int pitch);
    private native void nativeSetVideoBuffer(ByteBuffer buffer);

增加設定幀格式及回撥函式:

    public void setVideoFormat(String format, int width, int height, int pitch){
        nativeSetVideoFormat(format, width, height, pitch);
    }

    public void setVideoCallback(ByteBuffer buffer, MediaPlayerCallback callback) {
        mBuffer = buffer;
        mCallback = callback;
        nativeSetVideoBuffer(buffer);
    }

2.3.2 Activity中呼叫
        LibVLC mLibVLC = new LibVLC(this);

        Uri uri = Uri.parse("http://www.w3school.com.cn/i/movie.ogg");
        Media m = new Media(mLibVLC, uri);

        final int FRM_WIDTH = 500;
        final int FRM_HEIGHT = 400;
        final int PIXEL_SIZE = 4;

        MediaPlayer mediaPlayer = new MediaPlayer(m);
        ByteBuffer frameBuffer = ByteBuffer.allocateDirect(FRM_WIDTH*FRM_HEIGHT*PIXEL_SIZE);
        mediaPlayer.setVideoFormat("RGBA",FRM_WIDTH,FRM_HEIGHT,FRM_WIDTH*PIXEL_SIZE);
        mediaPlayer.setVideoCallback(frameBuffer, new MediaPlayerCallback() {
            @Override
            public void onDisplay(ByteBuffer buffer) {
                buffer.rewind();
                final Bitmap bitmap = Bitmap.createBitmap(FRM_WIDTH,FRM_HEIGHT, Bitmap.Config.ARGB_8888);
                bitmap.copyPixelsFromBuffer(buffer);
                runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        imageView.setImageBitmap(bitmap);
                    }
                });
            }
        });

        mediaPlayer.setMedia(m);
        mediaPlayer.play();

至此就大功告成,可以獲取到視訊中的每一幀了。

四、存在的問題

4.1 解碼效率問題

有心的看官可能已經注意到了這段話,來看下官方對於libvlc_video_set_callbacks介紹,出自全知全能的

 * The following limitations affect performance:
 * - Hardware video decoding acceleration will either be disabled completely,
 *   or require (relatively slow) copy from video/DSP memory to main memory.
 * - Sub-pictures (subtitles, on-screen display, etc.) must be blent into the
 *   main picture by the CPU instead of the GPU.
 * - Depending on the video format, pixel format conversion, picture scaling,
 *   cropping and/or picture re-orientation, must be performed by the CPU
 *   instead of the GPU.
 * - Memory copying is required between LibVLC reference picture buffers and
 *   application buffers (between lock and unlock callbacks).

呼叫了libvlc_video_set_callbacks相當於在人家原有的完美的軟硬體結合的流程中橫叉了一槓子,變純軟體解碼了,字幕也需要CPU混合進影象了,video format的各種轉換也用CPU了,GPU罷工了,等等效率問題。其中的酸甜苦辣就只能根據應用場景自行判斷了。

4.2 顯示效率問題

像我的程式碼中這樣,把每一幀轉換成Bitmap進行顯示,就有一次額外的記憶體拷貝的過程,肯定不是最優方案,這個還沒細研究,估計是有解,小弟孤陋寡聞,現在還不知道怎麼搞。