RTMP攝像頭直播-CameraX數據采集處理

距離上一次寫東西,如果不翻記錄,是真想不起來是什么時候了,在記憶中,應該是三月的時候,或者更早了,因為那時候還沒有換工作。寫到這里還是忍不住去翻了一下以往的記錄,發現后來也有寫過兩篇,已經沒什么印象了。從3月到8月五個多月的時間,回憶起來仿佛就在昨天。這半年來,對于我來說變化實在太大,首先是離開了自己工作了三年多的公司,很多種原因,在離開的時候并沒有一絲絲不舍,此刻突然想到過往的一切,好多人好多事浮現在眼前,就在昨天看到前同事發的狀態,好多熟悉的面孔,那一刻又映入眼簾。是的,畢竟是自己曾經為之努力奮斗的地方,尤其在19年的時候,那時候真的用過心并努力過,也包括20年初期的時候。當然,隨之而來的就是擺爛了。剛出來那段時間有太多的不適應,首先是自己租了房子,吃飯都需要掏錢買,以至于覺得錢花的好快,在之前每收到工資我就直接轉到天天基金賬戶了,而現在得考慮房租和飯錢和其它日常開銷,在加上每天上班都需要擠地鐵,以至于前期那段時間整個人被整的一團糟。

我以為在我換了工作,激情會再一次被點燃,記得前段時間和一個玩的很好的初中同學聊天,談到現在的狀態,我談到如果再讓我選擇一次,我也堅決不會做這個行業了,我想愛惜好自己的眼睛,去當兵,然而再也沒有第二次選擇的機會了。有時候挺想做一條咸魚,但又無法做到最咸的那一條,既然當下選擇了,我覺得還是努力做好當下的事情,既然做了一天和尚,就應該努力把這個鐘敲好。當然經過歲月的洗禮,也有值得慶幸的地方,自己的心態也逐漸走向成熟,不再浮躁,而是能靜下心來,思考一些事情。

關于之前開源的項目,https://github.com/zhuhuitao/printer,前前后后一共迭代了10個版本左右,當時在做這件事情的時候,沒有想到會有這么多同學去看和使用,說實在的,很開心。也看到好幾個同學提了issues,和留言,由于我目前沒有從事相關工作,身邊也沒有打印機,所以已接近大半年沒有維護了,在此想跟大家說一聲抱歉,后續如果有機會我還是會把問題整理出來,統一解決,深感抱歉。

前言

在很久以前一直想轉音視頻方向,一直沒有機會,畢竟想跨入這個方向,確實有一些難度。雖然現在項目中也有音視頻相關的東西,無奈都不是我負責。人生嘛總會遇到容易的事情和困難的事情,如果總是逃避困難的事情,想想也沒有什么意義,當然適當強迫一下自己,或許會收到不一樣的結果。在學習的過程中,學會總結和輸出真的太重要了,如果別人看到后有收獲,當然是值得開心的了,更多的是自己在總結和輸出的時候,往往有更多的收獲和對某個知識的理解。

android設備直播流程

在使用Android設備進行攝像頭直播時,其過程應該是這樣的:


流程

就圖像而言,首先需要獲得攝像頭采集的數據,然后得到這個byte[]進行編碼,再進行后續的封包與發送。我們通過CameraX圖像分析接口得到的數據為ImageProxy(Image的代理類)。那么怎么從ImageProxy/Image中獲取我們需要的數據呢,這個數據格式是什么?

ImageProxy/Image

Image是android SDK提供的一個完整的圖像緩沖區,圖像數據為:YUV或者RGB等格式。在編碼時,一般編碼器接收的待編碼數據格式為I420。而ImageProxy則是CameraX中定義的一個接口,Image的所有方法,也都能夠從ImageProxy調用。可以通過image的getPlanes方法得到PlaneProxy數組,關于CameraX的詳細資料我們都可以在android官方文檔查看到。https://developer.android.google.cn/training/camerax?hl=zh_cn。當然CameraX給到我們的數據格式在官網中有提到,為YUV_420_888格式的圖片。

YUV420

YUV模型是根據一個亮度(Y分量)和兩個色度(UV分量)來定義顏色空間,常見的YUV格式有YUY2、YUYV、YVYU、UYVY、AYUV、Y41P、Y411、Y211、IF09、IYUV、YV12、YVU9、YUV411、YUV420等,其中比較常見的YUV420分為兩種:YUV420P和YUV420SP。其中Y表示亮度,U和V表示色度。( 如果UV數據都為0,那么我們將得到一個黑白的圖像。)RGB中每個像素點都有獨立的R、G和B三個顏色分量值,YUV根據U和V采樣數目的不同,分為如YUV444、YUV422和YUV420等,而YUV420表示的就是每個像素點有一個獨立的亮度表示,即Y分量;而色度,即U和V分量則由每4個像素點共享一個。舉例來說,對于4x4的圖片,在YUV420下,有16個Y值,4個U值和4個V值。YUV420根據顏色數據的存儲順序不同,又分為了多種不同的格式,這些格式實際存儲的信息還是完全一致的。舉例來說,對于4x4的圖片,在YUV420下,任何格式都有16個Y值,4個U值和4個V值,不同格式只是Y、U和V的排列順序變化。I420YYYYYYYYYYYYYYYYUUUUVVVV ,NV21 則為 YYYYYYYYYYYYYYYYUVUVUVUV 。也就是說,YUV420
是一類格式的集合,YUV420并不能完全確定顏色數據的存儲順序。
更詳細的介紹可以參考這篇文章https://zhuanlan.zhihu.com/p/495400095

PlaneProxy/Plane

Y、U和V三個分量的數據分別保存在三個 Plane 類中,即通過 getPlanes() 得到的數組。 Plane 實際是對ByteBuffer 的封裝。Image保證了planes[0]一定是Y,planes[1]一定是U,planes[2]一定是V。且對于plane [0],Y分量數據一定是連
續存儲的,中間不會有U或V數據穿插,也就是說我們一定能夠一次性得到所有Y分量的值。
但是對于UV數據,可能存在以下兩種情況:

  1. planes[1] = {UUUU...},planes[2] = {VVVV...};
  2. planes[1] = {UVUV...},planes[2] = {VUVU...}。
    所以在我么取數據時需要在根據Plane中的另一個信息來確定如何取對應的U或者V數據。
//行內數據值間隔
//1,表示無間隔取值,即為上面的第一種情況
//2,表示需要間隔一個數值取值,即為上面第二種情況
int pixelStride = plan.getPixelStride();

根據這個屬性,我們將確定數據如何存儲,因此如果需要取出代表I420格式的byte[],則為:

YUV420中,y數據的長度為:width*height,而u,v都為width/2*height/2.
int pixelStride = plans[0].getPixelStride();
planes[0].getBuffer()
byte [] = new byte[image.getWidth()/2*image.getHeight()/2];
int pixelStride = planes[1].getPixelStride();
if(pixelStride == 1){
  planes[1].getBuffer();//u數據
}else if(pixelStride == 2){
    ByteBuffer uBuffer = planes[1].getBuffer();
    for(int i = 0;i<uBuffer.remaining;i++){
    u[i] = uBuffer.get();//丟棄一個數據,這里其實是v數據
    uBuffer.get():
}
}

//v數據與u數據同樣獲取

但是如果使用上面的代碼去獲取I420數據,可能會驚奇的發現,并不是在所有設置的Width與Height(分辨率)下都能夠正常運行。我們忽略了什么,為什么會出現問題呢?在Plane中我們已經使用了 getBuffer 與 getPixelStride 兩個方法,但是還有一個 getRowStride 是干嘛的呢?

RowStride

RowStride表示行步長,Y數據對應的行步長可能為:

  1. 等于Width;
  2. 大于Width;
    以4x4的I420為例,其數據可以看為:
      Y   Y   Y   Y
      Y   Y   Y   Y
      Y   Y   Y   Y
      Y   Y   Y   Y
      U   U
      U   U
      V   V
      V   V

如果RowStride等于Width,那么我們直接通過 planes[0].getBuffer() 獲得Y數據沒有問題。
但是如果RowStride大于Width,比如對于4x4的I420,如果每行需要以8字節對齊,那么可能得到的RowStride不
等于4(Width),而是得到8。那么此時會在每行數據末尾補充占位的無效數據:

        Y   Y   Y   Y    0    0    0    0
        Y   Y   Y   Y    0    0    0    0
        Y   Y   Y   Y    0    0    0    0
        Y    Y  Y   Y    最后一行沒有占位

對于這種情況,我們獲取Y數據,則為:

 /**
         * Y數據
         */
        //y數據的這個值只能是:1
        int pixelStride = planes[0].getPixelStride();
        ByteBuffer yBuffer = planes[0].getBuffer();
        int rowStride = planes[0].getRowStride();

        //1、rowStride 等于Width ,那么就是一個空數組
        //2、rowStride 大于Width ,那么就是每行多出來的數據大小個byte
        byte[] skipRow = new byte[rowStride - image.getWidth()];
        byte[] row = new byte[image.getWidth()];
        for (int i = 0; i < image.getHeight(); i++) {
            yBuffer.get(row);
            i420.put(row);
            // 不是最后一行才有無效占位數據,最后一行因為后面跟著U 數據,沒有無效占位數據,不需要丟棄
            if (i < image.getHeight() - 1) {
                yBuffer.get(skipRow);
            }
        }

而對于U與V數據,對應的行步長可能為:

  1. 等于Width;
  2. 大于Width;
  3. 等于Width/2;
  4. 大于Width/2

等于width

這表示,我們獲得planes[1]中不僅包含U數據,還會包含V的數據,此時pixelStride==2

    U    V    U    V
    U    V    U    V

那么V數據:planes[2],則為:

    V    U    V    U
    V    U    V    U

這種情況下,我們上面的代碼也已經處理了。

大于width

與Y數據一樣,可能由于字節對齊,出現RowStride大于Width的情況,與等于Width一樣,planes[1]中不僅包含U數據,還會包含V的數據,此pixelStride==2。

      U    V    U    V    0    0    0    0
      U    V    U    V    最后一行沒有占位

planes[2],則為:

    V    U    V    U    0    0    0    0
    V    U    V    U    最后一行沒有占位

等于width/2

當獲取的U數據對應的RowStride等于Width/2,表示我們得到的planes[1]只包含U數據。此時pixelStride==1。那么planes[1]+planes[2]為:

    U    U
    U    U
    V    V
    V    V

這種情況,所有的U數據是連在一起的,即 planes[1].getBuffer 可以直接獲得完整的U數據。

大于width/2

同樣我們得到的planes[1]只包含U數據,但是與Y數據一樣,可能存在占位數據。此時pixelStride==1。planes[1]+planes[2]為:

    U    U    0    0    0    0    0    0
    U    U          最后一行沒有占位
    V    V    0    0    0    0    0    0
    V    V           最后一行沒有占位

總結

在獲得了攝像頭采集的數據之后,我們需要獲取對應的YUV數據,需要根據pixelStride判斷格式,同時還需要通過rowStride來確定是否存在無效數據,那么最終我們獲取YUV數據的完整實現為:

    public static byte[] getBytes(ImageProxy image, int rotationDegrees, int width, int height) {
        //圖像格式
        int format = image.getFormat();
        if (format != ImageFormat.YUV_420_888) {
            //拋出異常
        }

        ByteBuffer i420 = ByteBuffer.allocate(image.getWidth() * image.getHeight() * 3 / 2);
        // 3個元素 0:Y,1:U,2:V
        ImageProxy.PlaneProxy[] planes = image.getPlanes();
        // byte[]

        /**
         * Y數據
         */
        //y數據的這個值只能是:1
        int pixelStride = planes[0].getPixelStride();
        ByteBuffer yBuffer = planes[0].getBuffer();
        int rowStride = planes[0].getRowStride();

        //1、rowStride 等于Width ,那么就是一個空數組
        //2、rowStride 大于Width ,那么就是每行多出來的數據大小個byte
        byte[] skipRow = new byte[rowStride - image.getWidth()];
        byte[] row = new byte[image.getWidth()];
        for (int i = 0; i < image.getHeight(); i++) {
            yBuffer.get(row);
            i420.put(row);
            // 不是最后一行才有無效占位數據,最后一行因為后面跟著U 數據,沒有無效占位數據,不需要丟棄
            if (i < image.getHeight() - 1) {
                yBuffer.get(skipRow);
            }
        }

        /**
         * U、V
         */
        for (int i = 1; i < 3; i++) {
            ImageProxy.PlaneProxy plane = planes[i];
            pixelStride = plane.getPixelStride();
            rowStride = plane.getRowStride();
            ByteBuffer buffer = plane.getBuffer();

            //每次處理一行數據
            int uvWidth = image.getWidth() / 2;
            int uvHeight = image.getHeight() / 2;

            // 一次處理一個字節
            for (int j = 0; j < uvHeight; j++) {
                for (int k = 0; k < rowStride; k++) {
                    //最后一行
                    if (j == uvHeight - 1) {
                        //uv沒混合在一起
                        if (pixelStride == 1) {
                            //rowStride :大于等于Width/2
                            // 結合外面的if:
                            //  如果是最后一行,我們就不管結尾的占位數據了
                            if (k >= uvWidth) {
                                break;
                            }
                        } else if (pixelStride == 2) {
                            //uv混在了一起
                            // rowStride:大于等于 Width
                            if (k >= image.getWidth()) {
                                break;
                            }
                        }
                    }


                    byte b = buffer.get();
                    // uv沒有混合在一起
                    if (pixelStride == 1) {
                        if (k < uvWidth) {
                            i420.put(b);
                        }
                    } else if (pixelStride == 2) {
                        // uv混合在一起了
                        //1、偶數位下標的數據是我們本次要獲得的U/V數據
                        //2、占位無效數據要丟棄,不保存
                        if (k < image.getWidth() && k % 2 == 0) {
                            i420.put(b);
                        }
                    }
                }
            }
        }


        //I420
        byte[] result = i420.array();

        if (rotationDegrees == 90 || rotationDegrees == 270) {
            //旋轉之后 ,圖像寬高交換
            result = rotation(result, image.getWidth(), image.getHeight(), rotationDegrees);
        }

        return result;
    }
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,622評論 6 544
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,716評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,746評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,991評論 1 318
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,706評論 6 413
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 56,036評論 1 329
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,029評論 3 450
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,203評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,725評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,451評論 3 361
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,677評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,161評論 5 365
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,857評論 3 351
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,266評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,606評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,407評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,643評論 2 380

推薦閱讀更多精彩內容