本文記錄的是從AVFrame到Bitmap的實現過程,爲了突出重點,FFmpeg解碼視頻文件得到AVFrame的過程不在這裏記錄,如需要了解,可以看下【Samples】demuxing_decoding
目的
前提:假定我們已經通過FFmpeg解碼視頻文件獲取到AVFrame了。
實現從AVFrame到Bitmap的轉換。
Native層創建Bitmap
這個bitmap也可以由Java層傳遞過來,不過我們這裏假設Java層只給了我們一個視頻文件的路徑。
底層創建Bitmap,也是一寫JNI方面的操作了,這裏給出提供一個create_bitmap函數:
jobject create_bitmap(JNIEnv *env, int width, int height) {
// 找到 Bitmap.class 和 該類中的 createBitmap 方法
jclass clz_bitmap = env->FindClass("android/graphics/Bitmap");
jmethodID mtd_bitmap = env->GetStaticMethodID(
clz_bitmap, "createBitmap",
"(IILandroid/graphics/Bitmap$Config;)Landroid/graphics/Bitmap;");
// 配置 Bitmap
jstring str_config = env->NewStringUTF("ARGB_8888");
jclass clz_config = env->FindClass("android/graphics/Bitmap$Config");
jmethodID mtd_config = env->GetStaticMethodID(
clz_config, "valueOf", "(Ljava/lang/String;)Landroid/graphics/Bitmap$Config;");
jobject obj_config = env->CallStaticObjectMethod(clz_config, mtd_config, str_config);
// 創建 Bitmap 對象
jobject bitmap = env->CallStaticObjectMethod(
clz_bitmap, mtd_bitmap, width, height, obj_config);
return bitmap;
}
然後,我們調用該函數,獲取bimtap對象:
jobject bitmap = create_bimap(env, frame->width, frame->height);
獲取Bitmap像素數據地址,並鎖定
void *addr_pixels;
AndroidBitmap_lockPixels(env, bitmap, &addr_pixels);
解釋一下這兩句話:
- 第一句的作用聲明並定義一個指向任意類型的指針變量,名稱是addr_pixels。我們定義它的目的,是讓它指向bitmap像素數據(即: addr_pixels的值爲bitmap像素數據的地址)。注意哦,這時候,addr_pixels的值是一個隨機的值(假定此時爲:0x01),由系統分配,它還不指向bitmap像素數據。
- 第二句話的作用就是將bitmap的像素數據地址賦值給addr_pixels,此時它的值被修改(假定爲:0x002)。並且鎖定該地址,保證不會被移動。【注:地址不會被移動這裏我也不太懂什麼意思,有興趣的可以去查看該方法的API文檔】
【注:】此時的bitmap由像素數據的地址,但是該地址內還沒有任何像素數據哦,或者說它的像素數據爲\0
到這裏,我們已經有了源像素數據在AVFrame中,有了目的像素數據地址addr_pixels,那麼接下來的任務就是將AVFrame中的像素數據寫入到addr_pixels指向的那片內存中去。
向Bitmap中寫入像素數據
這裏要說一下,我們獲取到的AVFrame的像素格式通常是YUV格式的,而Bitmap的像素格式通常是RGB格式的。因此我們需要將YUV格式的像素數據轉換成RGB格式進行存儲。而RGB的存儲空間Bitmap不是已經給我門提供好了嗎?嘿嘿,直接用就OK了,那現在問題就是YUV如何轉換成RGB呢?
關於YUV和RGB之間的轉換,我知道的有三種方式:
- 通過公式換算
- FFmpeg提供的libswscale
- Google提供的libyuv
這裏我們選擇libyuv因爲它的性能好、使用簡單。
說它使用簡單,到底有多簡單,嘿,一個函數就夠了!!
libyuv::I420ToABGR(frame->data[0], frame->linesize[0], // Y
frame->data[1], frame->linesize[1], // U
frame->data[2], frame->linesize[2], // V
(uint8_t *) addr_pixels, linesize, // RGBA
frame->width, frame->height);
解釋一下這個函數:
- I420ToABGR: I420表示的是YUV420P格式,ABGR表示的RGBA格式(execuse me?? 是的,你沒看錯,Google說RGBA格式的數據在底層的存儲方式是ABGR,順序反過來,看下libyuv源碼的函數註釋就知道了)
- frame->data&linesize: 這些個參數表示的是源YUV數據,上面有標註
- (uint8_t *) addr_pixels: 嘿,這個就是說往這塊空間裏寫入像素數據啦
-
linesize: 這個表示的是該圖片一行數據的字節大小,Bitmap按照RBGA格式存儲,也就是說一個像素是4個字節,那麼一行共有:frame->width 個像素,所以:
linesize = frame-> width * 4
【注:】關於這一小塊功能的實現,可能其他地方你會看到這樣的寫法,他們用瞭如下接口:
// 思路是:新建一個AVFrame(RGB格式),通過av_image_fill_arrays來實現AVFrame(RGB)中像素數據和Bitmap像素數據的關聯,也就是讓AVFrame(RGB)像素數據指針等於addr_pixels
pRGBFrame = av_frame_alloc()
av_image_get_buffer_size()
av_image_fill_arrays()
/*
我也是寫到這裏的時候,纔想到這個問題,爲什麼要這樣用呢,直接使用addr_pixels不是也一樣可以麼?
不過大家都這麼用,應該是有它不可替代的使用場景的。因此這裏也說一下av_image_fill_arrays這個函數。
*/
// TODO: 解釋下這個函數的作用
av_image_fill_arrays(dst_data, dst_linesize, src_data, pix_fmt, width, height, align);
它的作用就是
1. 根據src_data,設置dst_data,事實上根據現象或者自己去調試,可以發現dst_data的值就是src_data的值(我印象中好像值是相同的,這會我忘了,後面我再驗證下)
2. 根據pix_fmt, width, height設置linesize的值,其實linesize的計算就和我上面給出的那個公式是一樣子的值
OK, 函數執行完畢,我們Bitmap就有了像素數據,下面就是把Bitmap上傳給Java層
Native回調Java接口
說下Java層
- 有一個MainActivity.java用於界面的顯示
有一個JNIHelper.java用於Java層和Native層的溝通
public class JNIHelper { public void onReceived(Bitmap bitmap){ // TODO: Java層接收到Bitmap後,可以開始搞事情了 } }
Native層的回調代碼如下:
jclass clz = env->FindClass("me/oogh/xplayer/JNIHelper");
jmethodID method = env->GetMethodID(clz, "onReceived", "(Landroid/graphics/Bitmap;)V");
env->CallVoidMethod(obj, method, bitmap);
同樣也解釋一下:
- FindClass: 找到JNIHelper類
- GetMethodID: 找到JNIHelper類中的void onReceived(Bitmap bitmap)方法
- CallVoidMethod: 剛開始我們創建的bitmap對象,作爲參數,執行onReceived
至此,從AVFrame到Bitmap,再將Bitmap上傳,就已經完成了。
參考鏈接
- Android JNI 之 Bitmap操作:https://juejin.im/post/5b5810...
- Bitmap | Android NDK:https://developer.android.com...