1. 程式人生 > >Android使用FFmpeg 解碼H264並播放(三)

Android使用FFmpeg 解碼H264並播放(三)

上一節記錄了Android使用FFmpeg解碼H264的過程。這一節記錄在Android上播放的過程。

問題描述

在開發中使用某攝像頭的SDK,只能獲取到一幀幀的 H264 視訊資料,不知道視訊流地址,需要自己解碼出影象並播放。

問題解決

Android 播放解碼後的視訊幀

在Android上播放視訊的總體思路是在Native層從 Surface 獲取 到ANativeWindow,通過修改 ANativeWindow 的顯示 buffer 來更新 Surface 的畫面,這樣一幀幀更新,最終看到的就是動畫。

AVFrame 格式轉化

一般從視訊流裡解析出的 AVFrame 的格式是 yuv 的,不能直接在 Android 的 nativewindow 上使用,需要先轉換為 ARGB 或者 RGB565才行。
轉碼的程式碼網上很多,大致如下:

//1. 準備一個容器來裝轉碼後的資料
AVFrame dst_frame = av_frame_alloc();
//在解碼上下文使用extradata解析出第一幀影象後,ctx的width和height,pix_format 寫入了實際的視訊寬高,畫素格式
dst_frame->width = ctx->width; 
dst_frame->height = ctx->height;
//2. 轉碼為ARGB,來給NativeWindow顯示
dst_frame->format = AV_PIX_FMT_ARGB;
//3. 根據輸入影象和輸出影象的資訊(寬、高、畫素),初始化格式轉換上下文
//應該重複使用該上下文,不要每一幀都初始化一次 struct SwsContext * swsCtx = sws_getContext( src_frame->width, src_frame->height,(enum AVPixelFormat) src_frame->format, src_frame->width, src_frame->height,(enum AVPixelFormat) dst_frame->format, SWS_FAST_BILINEAR, NULL, NULL, NULL); //4. 初始化之前準備的dst_frame的buffer
int buffer_size = av_image_get_buffer_size( (enum AVPixelFormat) dst_frame->format, src_frame->width, src_frame->height, 1); uint8_t * buffer = (uint8_t *) av_malloc(sizeof(uint8_t) * buffer_size ); //5. 繫結dst_frame和新申請的buffer av_image_fill_arrays(dst_frame->data, dst_frame->linesize, buffer, (enum AVPixelFormat) dst_frame->format, dst_frame->width, dst_frame->height, 1); //6. 轉碼 sws_scale(swsCtx , (const uint8_t *const *) src_frame->data, src_frame->linesize, 0, src_frame->height, dst_frame->data, dst_frame->linesize);
渲染畫面到 NativeWindow
  1. 從surface 中獲取 NativeWindow
  2. 設定NativeWindow緩衝區格式
  3. 鎖定NativeWindow緩衝區
  4. 寫入資料到緩衝區
  5. 釋放緩衝區
  6. 如果不再渲染了,釋放NativeWindow

程式碼如下:

  • H264FrameRender.java
/**
* 設定渲染的Surface
*/
public void setSurface(Surface surface) {
       if (nativeObject <= 0) {
           throw new IllegalStateException("H264FrameRender init failed");
       }
       _setSurface(this.nativeObject, surface);
   }
/**
* JNI
*/   
private native void _setSurface(long nativeObject, Surface surface);
  • JNI
JNIEXPORT void JNICALL Java_com_demo_H264FrameRender__1setSurface(JNIEnv *env, jobject instance,jlong nativeObject, jobject surface){
    //
    H264Render *render = (H264Render *) nativeObject;
    //獲取到NativeWindow,
    ANativeWindow* window = ANativeWindow_fromSurface(surface);
    //綁定當前的window到native 層的render上,等待渲染
    if(render->window){
        ANativeWindow_release(render->window);
        render->window = NULL;
    }
    render->window = window;    
}
  • 渲染
    解碼器和格式轉換器完成後會將呼叫渲染方法,將最終的資料渲染到 NativeWindow 上。

/**
* @param render 渲染器
* @param data   解碼和轉換處理後的影象資料
* @param len    data 的長度
* @param width  視訊的寬
* @param height 視訊的高
*/
void render_rend(NativeRender *render, signed char *data, size_t len, int width, int height) {
    if (render->window == NULL) {
        LOGE("window == null");
        return;
    }
    //設定緩衝區(寬,高,畫素格式)
    ANativeWindow_setBuffersGeometry(render->window, width, height, WINDOW_FORMAT_RGBA_8888);
    //繪製
    int32_t locked = ANativeWindow_lock(render->window, render->buffer, NULL);

    if (locked == 0) {
        //由於window的 stride 和 frame 的 stride 不同,需要逐行拷貝
        uint8_t *dst = (uint8_t *) render->buffer->bits;
        uint8_t *src = dst_frame->data[0];
        int dest_stride = buffer->stride * 4;
        int src_stride = dst_frame->linesize[0];
        int height = dst_frame->height, h;
        for (h = 0; h < height; h++) {
            memcpy(dst + h * dest_stride,
                   src + h * src_stride,
                   (size_t) src_stride);
        }

        ANativeWindow_unlockAndPost(render->window);
    } else {
        LOGD("failed to lock window");
    }

}