Camera直播視頻數據的獲取,及RTMP推流(一)

通過攝像頭直播推流的場景中,需要先從攝像頭獲取去視頻元數據,然後交給x264編碼器(加入用的視頻編碼器是x264)編碼,最後經RTMP封包後發送給服務器.

我們使用CameraX來獲取攝像頭數據,對於CameraX的使用,參考官方文檔:

https://developer.android.google.cn/training/camerax

CameraX 是一個 Jetpack 支持庫,旨在幫助您簡化相機應用的開發工作。它提供一致且易於使用的 API 界面,適用於大多數 Android 設備,並可向後兼容至 Android 5.0(API 級別 21).

CameraX 引入了多個用例,

預覽:在顯示屏上顯示圖片

圖片分析:無縫訪問緩衝區以便在算法中使用,

圖片拍攝:保存優質圖片

一,

在直播這個應用中,主要關注的圖片分析.簡單列出CameraX使用的代碼:

<uses-permission android:name="android.permission.CAMERA"/>

佈局文件中使用TextureView顯示預覽效果:

    <TextureView
        android:id="@+id/textureView"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>

在MainActivity中使用

CameraX.bindToLifecycle((LifecycleOwner) this, getPreview(), getAnalysis());

    //Analysis和Priview中設置的分辨率可以不同,這說明分析圖片和預覽圖片是可以分開應用的。
    private ImageAnalysis getAnalysis() {
        ImageAnalysisConfig analysisConfig = new ImageAnalysisConfig.Builder()
                .setCallbackHandler(new Handler(handlerThread.getLooper()))
                .setLensFacing(CameraX.LensFacing.BACK)
                .setImageReaderMode(ImageAnalysis.ImageReaderMode.ACQUIRE_LATEST_IMAGE)
                .setTargetResolution(new Size(480, 640))
                .build();
        ImageAnalysis imageAnalysis = new ImageAnalysis(analysisConfig);
        imageAnalysis.setAnalyzer(this);
        return imageAnalysis;
    }

    private Preview getPreview() {
        //這裏給出的分辨率,並不是最終的值,CameraX會根據設備的支持情況,設置一個最接近你給定參數的值。
        PreviewConfig previewConfig= new PreviewConfig.Builder().
                setTargetResolution(new Size(480, 640)).
                setLensFacing(CameraX.LensFacing.BACK).build();
        Preview preview = new Preview(previewConfig);
        preview.setOnPreviewOutputUpdateListener(this);
        return preview;
    }

二,粗略看下預覽的實現,

這個紋理,就是攝像頭採集到的一張圖片,把這個紋理設置到TextureView,就可以預覽出圖像 ,SurfaceTexture對數據流的處理,並不是直接顯示,而是轉爲GL的外部紋理, 因此可用於圖像數據的二次處理(濾鏡,美顏), Camera的預覽數據,一種情況,可以變成紋理後交給GLSurfaceView直接顯示,還有一種情況, 預覽數據通過SurfaceTexture交給TextureView作爲View Heirachy的一個硬件加速層來顯示, CamerX的預覽就是後一種情況.

TextureView可以把內容流直接投影到View中。 SurfaceTexture從圖像流(Camera預覽,視頻解碼,GL繪製)中獲得幀數據,當調用UpdateTexImage時,  根據內容流最近的圖像更新SurfaceTexture對應的紋理對象,然後就可以向操作普通紋理一樣去操作它了。 TextureView不會在WMS中創建單獨的窗口,而且必須在硬件加速窗口中,在它的draw方法中,  把SurfaceTexture中收到的圖像數據作爲紋理更新到對應的Hardwarelayer中。

SurfaceTexture,TextureView的詳細介紹:https://www.cnblogs.com/wytiger/p/5693569.html

    @Override
    public void onUpdated(Preview.PreviewOutput output) {
        SurfaceTexture surfaceTexture = output.getSurfaceTexture();
        if (mTextureView.getSurfaceTexture() !=  surfaceTexture) {
            if (mTextureView.isAvailable()) {
                //這裏的處理,避免切換攝像頭時出錯。
                ViewGroup parent = (ViewGroup) mTextureView.getParent();
                parent.removeView(mTextureView);
                parent.addView(mTextureView, 0);
                parent.requestLayout();
            }
            mTextureView.setSurfaceTexture(surfaceTexture);
        }
    }

三,圖像分析,

x264編碼器需要的數據格式是I420, 通過Camera進行直播的過程,首先,從Camera獲取數據,這個數據就是一個byte數組,然後把byte數據送去編碼, 編碼之後,按照rtmp的格式進行封包,把封包後的數據通過socket發送出去。 這裏藉助CameraX來獲取圖像數據,通過CameraX的圖像分析接口得到的數據是ImageProxy, 通過ImageProxy可以得到我們想要的圖像的byte數組。因爲CameraX生成是YUV420-888的格式的圖片,所以我們得 到的圖像數據格式就是YUV格式的。我們要從YUV_420_888數據中提取出byte數組,交給x264去編碼。

    //這裏可以得到圖片的數據,寬高,格式,時間戳等,及旋轉角度。
    @Override
    public void analyze(ImageProxy imageProxy, int rotationDegrees) {
        Image image = imageProxy.getImage();
        //這個數組的第0個元素保存的是Y數據,data[0].getBuffer();
        //這個數組的第1個元素保存的是U數據,
        //這個數組的第2個元素保存的是V數據,
        //Image.Plane[] data = image.getPlanes();
        byte[] dataI420 = ImageUtils.getBytes(imageProxy, rotationDegrees, mWidth, mHeight);
    }

從YUV420-888中提取byte[]數組.

YUV420這類格式的圖像,4個Y分量共用一組UV分量,根據顏色數據的存儲順序不同,又分了幾種不同格式,這些格式實際存儲的信息是一樣的,如:4*4的圖片,在YUV420下,任何格式都是16個Y值,4個U值,4個V值,區別只是Y,U,V的排列順序不同.YUV420是一類顏色格式的集合,並不能完全確定顏色數據的存儲順序.

CameraX的應用中,YUV三個分量的數據分別保存在imageProxy.getPlanes()對應的數組中.

YUV420中,Y數據長度爲: width*height , 而U、V都爲:width / 2 * height / 2。

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...}。

具體UV的排列順序是那種情況,需要根據int pixelStride = plane.getPixelStride();來判斷:

pixelStride 爲1:表示無間隔取值,即爲上面的第一種情況
pixelStride 爲 2: 表示需要間隔一個數據取值,即爲上面的第二種情況

但是,考慮到Camera有不同的分辨率,所以在YUV數據存儲時有字節對齊的想象,就要考慮佔位符的問題. 因爲涉及補位的問題,要去考慮行步長planes[0].getRowStride()。 當Camera的分辨率不同時,補位數據的長度就會不一樣,planes[0].getRowStride()行步長也就會不一樣。

首先分析Y數據, RowStride表示行步長,Y數據對應的行步長可能爲:
1. 等於 imageProxy.getWidth() ;
2. 大於imageProxy.getWidth() ;
以4x4的I420爲例,其數據可以看爲:

rowStride等於width的情況,直接通過 planes[0].getBuffer() 獲得Y數據沒有問題

 rowStride大於width的情況,此時讀取數據時就要跳過尾部的佔位符,不然可能會報指針越界異常.

然後看UV數據,對於U與V數據,對應的行步長可能爲:
1. 等於Width;
2. 大於Width;
3. 等於Width/2;
4. 大於Width/2

在RowStride等於width時,並且pixelStride==2,表示UV交叉存放,如圖planes[1],此時在獲取U數據時,只獲取偶數位置的數據,奇數位置的數據丟棄.只從planes[1]中取U數據,

同理,我們只從planes[2]中取V數據,獲取偶數位置的數據,奇數位置的數據丟棄.

當RowStride大於width時,planes[1],同樣要跳過尾部的佔位符,注意最後一行沒有佔位符,不需要跳過.

當RowStride大於width時,planes[2],同樣要跳過尾部的佔位符,注意最後一行沒有佔位符,不需要跳過.

當RowStride等於Width/2,就是pixelStride==1,表示UV沒有交叉存放,都是單獨存放的,如:planes[1]

當RowStride等於Width/2,就是pixelStride==1,表示UV沒有交叉存放,都是單獨存放的,如planes[2]

當RowStride等於Width/2,就是pixelStride==1,表示UV沒有交叉存放,都是單獨存放的,但是有佔位符,此時,需要跳過佔位符,如planes[1]

當RowStride等於Width/2,就是pixelStride==1,表示UV沒有交叉存放,都是單獨存放的,但是有佔位符,此時,需要跳過佔位符,,如planes[2]

有了上面的分析,再看代碼實現,相信就不難理解了:

下面的函數是從ImageProxy中提取I420字節數組:


    public static byte[] getBytes(ImageProxy imageProxy, int rotationDegrees, int mWidth, int mHeight) {
        //獲取圖像格式
        int format = imageProxy.getFormat();
        //根據CameraX的官方文檔,CameraX返回的數據格式:YUV_420_888
        if (format != ImageFormat.YUV_420_888) {
            //異常處理,如果有廠商修改了CameraX返回的數據格式。
        }
        //I420的數據格式,4個Y共用一組UV,其中Y數據的大小是width * height,
        // U V數據的大小都是 (width/2) * (height/2)

        //這個數組的第0個元素保存的是Y數據,data[0].getBuffer(); Y分量數據時連續存儲的,
        //這個數組的第1個元素保存的是U數據,U V分量可能出現交叉存儲,
        //這個數組的第2個元素保存的是V數據,
        //如果按照上面的方式,簡單的獲取到Y U V分量的字節數組,然後拼接到一起,在多數情況下可能是正常的,
        // 但是不能兼容多種camera分辨率,因爲涉及補位的問題,要去考慮行步長planes[0].getRowStride()。
        // 當Camera的分辨率不同時,補位數據的長度就會不一樣,planes[0].getRowStride()行步長也就會不一樣。
        ImageProxy.PlaneProxy[] planes = imageProxy.getPlanes();
        //一個YUV420數據,需要的字節大小,
        int size = imageProxy.getWidth() * imageProxy.getHeight() * 3/ 2;
        if (null == yuvI420 || yuvI420.capacity() < size) {
            yuvI420 = ByteBuffer.allocate(size);
        }
        yuvI420.position(0);

        //獲取Y數據,getPixelStride 在Y分量下,總是1,表示Y數據時連續存放的。
        int pixelStride = planes[0].getPixelStride();
        ByteBuffer yBuffer = planes[0].getBuffer();
        //行步長,表示一行的最大寬度,可能等於imageProxy.getWidth(),
        // 也可能大於imageProxy.getWidth(),比如有補位的情況。
        int rowStride = planes[0].getRowStride();
        //這個字節數組表示,在讀取Y分量數據時,跳過補位的部分,因爲這些補位的部分沒有實際數據,只是爲了字節對齊,
        // 如果沒有補位 rowStride 等於 imageProxy.getWidth(),這就是一個空數組,否則,數組的長度就剛好是要跳過的長度。
        byte[] skipRow = new byte[rowStride - imageProxy.getWidth()];
        //這個數組,表示了這一行真實有效的數據。
        byte[] row = new byte[imageProxy.getWidth()];
        //循環讀取每一行
        for (int i = 0; i < imageProxy.getHeight(); i++) {
            yBuffer.get(row);
            //每一行有效的數據拼接起來就是Y數據。
            yuvI420.put(row);
            //因爲最後一行,沒有無效的補位數據,不需要跳過,不是最後一行,才需要跳過無效的佔位數據。
            if (i < imageProxy.getHeight() - 1) {
                yBuffer.get(skipRow);
            }
        }

        // 獲取U V數據
        // Y分量數據時連續存儲的,U V分量可能出現交叉存儲,
        pixelStride = planes[1].getPixelStride();
        // 如果pixelStride值爲1,表示UV是分別存儲的,planes[1] ={UUUU},planes[2]={VVVV},
        // 這個情況還是比較容易獲取的,

        // 如果pixelStride 爲2,表示UV是交叉存儲的,planes[1] ={UVUV},planes[2]={VUVU},
        // 這個情況,要獲取UV,就要拿一個,丟一個,交替取出,同時,也需要考慮跳過無效的補位數據。
        for (int i =0; i< 3; i++) {
            ImageProxy.PlaneProxy planeProxy =  planes[i];
            int uvPixelStride = planeProxy.getPixelStride();
            //如果U V是交錯存放,行步長就等於imageProxy.getWidth(),同時要考慮有佔位數據,會大於imageProxy.getWidth()
            // 如果U V是分離存放,行步長就等於imageProxy.getWidth() /2,同時要考慮有佔位數據,會大於imageProxy.getWidth()/2
            int uvRowStride = planeProxy.getRowStride();
            ByteBuffer uvBuffer = planeProxy.getBuffer();

            //一行一行的處理,uvWidth表示了有效數據的長度。
            int uvWidth = imageProxy.getWidth() / 2;
            int uvHeight = imageProxy.getHeight() / 2;

            for (int j = 0; j< uvHeight; j++) {
                //每次處理一行中的一個字節。
                for (int k =0; k < uvRowStride; k++) {
                    //跳過最後一行沒有佔位的數據,
                    if (j == uvHeight -1) {
                        //UV沒有混合在一起,
                        if (uvPixelStride == 1) {
                            //大於有效數據後,跳出內層循環(k < uvRowStride),不用關心最後的佔位數據了。
                            // 因爲最後一行沒有佔位數據。
                            if (k >= uvWidth) {
                                break;
                            }
                        } else if (uvPixelStride == 2) {
                            //UV沒有混合在一起,大於有效數據後,跳出內層循環(k < uvRowStride),
                            // 不用關心最後的佔位數據了。
                            // 因爲最後一行沒有佔位數據。注意這裏的有效數據的寬度是imageProxy.getWidth()。
                            //這裏爲什麼要減1呢?因爲在UV混合模式下,常規情況是UVUV,但是可能存在UVU的情況,
                            // 就是最後的V是沒有的,如果不在這裏 減1,在接下來get時,會報越界異常。
                            if (k >= imageProxy.getWidth() -1) {
                                break;
                            }
                        }
                    }
                    //對每一個字節,分別取出U數據,V數據。
                    byte bt =uvBuffer.get();
                    //uvPixelStride == 1表示U V沒有混合在一起
                    if (uvPixelStride == 1) {
                        //k < uvWidth表示是在有效範圍內的字節。
                        if (k < uvWidth) {
                            yuvI420.put(bt);
                        }
                    } else if (uvPixelStride == 2) {
                        //uvPixelStride == 2 表示U V混合在一起。只取偶數位小標的數據,纔是U / V數據,
                        // 奇數位,佔位符數據都丟棄,同時這裏的有效數據長度是imageProxy.getWidth(),
                        // 而不是imageProxy.getWidth() /2,
                        if (k < imageProxy.getWidth() && (k % 2 == 0)){
                            yuvI420.put(bt);
                        }
                    }

                }
            }
        }

        //全部讀取到YUV數據,以I420格式存儲的字節數組。
        byte[] result = yuvI420.array();
        //Camera 角度的旋轉處理。分別以順時針旋轉Y, U ,V。
        if (rotationDegrees == 90 || rotationDegrees == 270) {
            result = rotation(result, imageProxy.getWidth(), imageProxy.getHeight(), rotationDegrees);
        }
        return result;
    }

在獲取到攝像頭數據的byte數組後,還要考慮角度旋轉,藉助libyuv實現角度的順時針旋轉

extern "C"
JNIEXPORT jbyteArray JNICALL
Java_com_test_cameraxlive_ImageUtils_rotation(JNIEnv *env, jclass clazz, jbyteArray data_,
                                              jint width, jint height, jint degree) {
    jbyte *data = env->GetByteArrayElements(data_, 0);
    uint8_t *src = reinterpret_cast<uint8_t *>(data);
    int ySize = width * height;
    int uSize = (width >>1) * (height >>1);
    //Y U V總的數據大小
    int size = (ySize * 3) >> 1;
    uint8_t dst[size];

    //原始數據的Y數據,U數據,V數據
    uint8_t *src_y = src;
    uint8_t *src_u = src + ySize;
    uint8_t *src_v = src + ySize + uSize;
    //旋轉後的Y數據,U數據,V數據。
    uint8_t *dst_y = dst;
    uint8_t *dst_u = dst + ySize;
    uint8_t *dst_v = dst +ySize + uSize;

    libyuv::I420Rotate(src_y, width,
                       src_u, width>>1,
                       src_v, width>>1,
                       dst_y, height,
                       dst_u, height>>1,
                       dst_v, height>>1,
                       width, height, static_cast<libyuv::RotationMode>(degree));
    jbyteArray  result = env->NewByteArray(size);
    env->SetByteArrayRegion(result, 0, size, reinterpret_cast<const jbyte *>(dst));
    env->ReleaseByteArrayElements(data_, data, 0);
    return result;
}

到這裏,就完成了攝像頭數據的byte[]數組的提取.

接下來通過x264庫完成編碼:

H264的基礎知識,參考:https://blog.csdn.net/qq_29350001/article/details/78226286

使用X264編碼的流程:

1,設置 x264_param_t編碼器參數,通過 x264_encoder_open 創建一個 x264_t *codec編碼器

2,通過 x264_picture_alloc爲編碼器的輸入數據 x264_picture_t pic_in 申請內存,

3,通過 x264_encoder_encode完成編碼,

創建編碼器的代碼:

void VideoChannel::openCodec(int width, int height, int fps, int bitrate) {
    //編碼器的參數,以延遲最低爲目標配置。
    x264_param_t param;
    //第二,三個參數來自這兩個數組:x264_preset_names[],x264_tune_names[]  ,表示編碼速度,和質量控制,
    // zerolatency,無延遲編碼,主要用於實時通訊
    x264_param_default_preset(&param, "ultrafast", "zerolatency");
    //指定編碼規格,base_line 3.2 ,無B幀(雙向參考幀),數據量小,但是解碼速度慢。
    param.i_level_idc = 32;
    //輸入數據格式
    param.i_csp = X264_CSP_I420;
    //寬高,
    param.i_width = width;
    param.i_height = height;
    //指定無B幀
    param.i_bframe = 0;
    //表示碼率控制,CQP(恆定質量),CRF(恆定碼率),ABR(平均碼率)
    param.rc.i_rc_method = X264_RC_ABR;
    //碼率,單位kbps,
    param.rc.i_bitrate = bitrate / 1000;
    //最大碼率,
    param.rc.i_vbv_max_bitrate = (bitrate / 1000) * 1.2;
    //幀率
    param.i_fps_num = fps;
    param.i_fps_den = 1;
    //打開log輸出,查看編碼過程的日誌,這裏是指定日誌的回調。
    //param.pf_log = x264_log_default2;
    //關鍵幀間隔,
    param.i_keyint_max = fps * 2;
    //是否複製sps和pps放在每個關鍵幀的前面 該參數設置是讓每個關鍵幀(I幀)都附帶sps/pps。
    param.b_repeat_headers = 1;
    //不使用並行編碼。zerolatency場景下設置param.rc.i_lookahead=0;
    // 那麼編碼器來一幀編碼一幀,無並行、無延時
    param.i_threads = 1;
    param.rc.i_lookahead = 0;
    x264_param_apply_profile(&param, "baseline");

    codec = x264_encoder_open(&param);
    ySize = width * height;
    uSize = (width >> 1) * (height >> 1);
    this->width = width;
    this->height = height;
}

編碼數據的實現:

void VideoChannel::encode(uint8_t *data) {
    //要編碼的輸入數據
    x264_picture_t pic_in;
    //爲輸入數據,申請內存,指定格式,寬高,
    x264_picture_alloc(&pic_in, X264_CSP_I420, width, height);
    //把I420數據中YUV塞到pic_in結構體中
    pic_in.img.plane[0] = data;
    pic_in.img.plane[1] = data + ySize;
    pic_in.img.plane[2] = data + ySize + uSize;
    //這個pts每次編碼時需要增加,編碼器把它當做圖像的序號。
    pic_in.i_pts = i_pts++;

    //也可以從pic_out中拿到編碼後的數據,我們這裏是從pp_nal中獲取。
    x264_picture_t pic_out;
    //二級指針,保存編碼後的數據
    x264_nal_t *pp_nal;
    //編碼後的數組有幾個元素
    int pi_nal;
    int error = x264_encoder_encode(codec, &pp_nal, &pi_nal, &pic_in, &pic_out);
    if (error <=0) {
        return;
    }

    int spslen;
    int ppslen;
    uint8_t  *sps;
    uint8_t  *pps;
    //拿到編碼後的數據
    for (int i = 0; i < pi_nal; ++i) {
        //開始碼之後的第一個字節的低5位,表示了NAL的類型,7(sps)或者 8(pps),
        int type = pp_nal[i].i_type;
        //對應幀的數據,
        uint8_t *p_payload = pp_nal[i].p_payload;
        //對應幀數據的長度,其中SPS,PPS不屬於幀的範疇。
        int i_payload = pp_nal[i].i_payload;
        if (type == NAL_SPS) {
            //得到SPS,不能直接發送出去,而是要等到跟PPS,組成一個RTMP_packet一起發送給服務器,
            // 所以這裏先把sps保存下來
            //H264的數據中,每個NAL之間是由00 00 00 01或者 00 00 01來分割,在00 00 00 01後面跟着就是
            // 這一幀的類型,
            spslen = i_payload - 4; //去掉間隔 00 00 00 01
            sps = (uint8_t *)alloca(spslen);//在棧上申請內存,不用手動釋放。
            memcpy(sps, p_payload +4, spslen);
        } else if (type == NAL_PPS) {
            ppslen = i_payload -4;//去掉間隔 00 00 00 01
            pps = (uint8_t *) alloca(ppslen);//在棧上申請內存,不用手動釋放。
            memcpy(pps, p_payload +4, ppslen);
            //sps,pps後面接着肯定是I幀,所以在發送I幀之前,先把sps,pps發送出去。
            sendVideoConfig(sps, pps, spslen, ppslen);
        } else {
            //發送正常的數據幀,包括關鍵幀,普通幀。
            sendFrame(type, p_payload, i_payload);
        }
    }
}

編碼完成後,通過RTMP發送到服務器,除了正常的數據幀,還有AVC序列頭信息。

對RTMP視頻的封包,參考flv的格式文檔:

對視頻數據封包,只需關注數據區部分,前面的11個字節,不用考慮。

視頻數據部分,分關鍵幀,非關鍵幀,

視頻數據中0x17:其中 1表示關鍵幀,7 表示 高級視頻編碼AVC,對於普通幀則是0x27。

AVCVIDEOPACKET的格式定義:

如果類型是0,表示接下來這一段數據時AVC序列頭,如果是0,表示接下來一段數據是視頻幀(關鍵幀,非關鍵幀都是0)。

AVC序列頭格式定義:

SPS,PPS是在編碼H264視頻數據時,放在關鍵幀前面的信息,指導解碼器如何參考這個關鍵幀解碼出B幀,P幀的內容。

 

通常情況,RTMP封包的視頻結構體:

所以,對視頻幀(關鍵幀,非關鍵幀)的RTMPPacket的字節大小是 5 + 4 + 裸數據。

對於SPS,PPS的RTMPPacket的字節大小是 5+ 8 +3+ spslen + ppslen,其中 8 +3 是AVC序列頭定義的長度。

AVC序列頭定義的 8 + 3 個字節具體定義:

依據上面的分析,再來看下面的封包代碼,應該很容易理解了:

 

視頻配置信息的封包發送,在發送I幀之前,要先發送SPS ,PPS,


void VideoChannel::sendVideoConfig(uint8_t *sps, uint8_t *pps, int spslen, int ppslen) {
    //把SPS,PPS封裝成一個RTMPPacket發送出去,要發送的這個數據的總大小,除了spslen,ppslen,還有AVC序列頭的長度,
    //AVC序列頭的長度,根據結構體定位是5+ 8 +3 ,所以總的數據包大小是5+ 8 +3+ spslen + ppslen
    int bodySize = 13 + spslen + 3 + ppslen;
    RTMPPacket *packet = new RTMPPacket;
    RTMPPacket_Alloc(packet, bodySize);
    //往rtmppacket中裝入數據
    int index = 0;
    packet->m_body[index++] = 0x17; //固定頭
    packet->m_body[index++] = 0x00;//類型,
    //composition time 0x000000
    packet->m_body[index++] = 0x00;
    packet->m_body[index++] = 0x00;
    packet->m_body[index++] = 0x00;
    //版本
    packet->m_body[index++] = 0x01;
    //編碼規格
    packet->m_body[index++] = sps[1];
    packet->m_body[index++] = sps[2];
    packet->m_body[index++] = sps[3];
    packet->m_body[index++] = 0xFF;
    //整個sps
    packet->m_body[index++] = 0xE1;
    //sps長度,2個字節
    packet->m_body[index++] = (spslen >> 8) & 0xFF;
    packet->m_body[index++] = spslen & 0xFF;
    memcpy(&packet->m_body[index], sps, spslen);
    index += spslen;

    //裝入pps
    packet->m_body[index++] = 0x01;
    //長度,同樣佔兩個字節。
    packet->m_body[index++] = (ppslen >> 8) & 0xFF;
    packet->m_body[index++] = ppslen & 0xFF;
    memcpy(&packet->m_body[index], pps, ppslen);
    //設置RTMPPacket的參數
    packet->m_packetType = RTMP_PACKET_TYPE_VIDEO;
    packet->m_nBodySize = bodySize;
    packet->m_headerType = RTMP_PACKET_SIZE_MEDIUM;
    //時間戳,解碼器端將根據這個時間戳來播放視頻,這裏sps,pps不是圖像幀,所以不需要時間戳。
    packet->m_nTimeStamp = 0;
    //使用相對時間
    packet->m_hasAbsTimestamp = 0;
    //這個通道的值沒有特別要求,但是不能跟rtmp.c中使用的相同。
    packet->m_nChannel = 0x10;
    //在使用完packet,要釋放。
    callback(packet);
}

視頻 關鍵幀,普通幀的封包發送:

void VideoChannel::sendFrame(int type, uint8_t *p_payload, int i_payload) {
    //關鍵幀,普通幀的發送
    //去掉間隔符號,當間隔符是0x00 0x00 0x00 0x01,有4個字節,
    if (p_payload[2] == 0x00) {
        i_payload -= 4;
        p_payload += 4;
    } else if (p_payload[2] == 0x01) {
        //當間隔符是0x00 0x00 0x01,有3個字節,
        i_payload -= 3;
        p_payload += 3;
    }
    //往RTMPPacket中裝入數據
    RTMPPacket *packet = new RTMPPacket;
    //對於關鍵幀,非關鍵幀,根據RTMPPacket的結構定義,僅有第一個字節0x17(關鍵幀), 0x27的區別,
    // 總數據的大小是5 + 4(數據長度)+裸數據
    int bodySize = 9 + i_payload;
    RTMPPacket_Alloc(packet, bodySize);
    RTMPPacket_Reset(packet);
    //非關鍵幀,0x27
    packet->m_body[0] = 0x27;
    //如果是關鍵幀,0x17
    if (type == NAL_SLICE_IDR) {
        packet->m_body[0] = 0x17;
    }
    //關鍵幀,非關鍵幀的類型都是0x01, sps,pps的類型是0x00
    packet->m_body[1] = 0x01;
    //時間戳,
    packet->m_body[2] = 0x00;
    packet->m_body[3] = 0x00;
    packet->m_body[4] = 0x00;
    //數據長度,佔4個字節,相當於把int轉成4個字節的byte數組
    packet->m_body[5] = (i_payload >> 24) & 0xFF;//先取高位1個字節,
    packet->m_body[6] = (i_payload >> 16) & 0xFF;
    packet->m_body[7] = (i_payload >> 8) & 0xFF;
    packet->m_body[8] = i_payload & 0xFF; //最後去低8位。
    //填入裸數據
    memcpy(&packet->m_body[9], p_payload, i_payload);
    //設置RTMPPacket的參數
    packet->m_hasAbsTimestamp = 0;
    packet->m_nBodySize = bodySize;
    packet->m_packetType = RTMP_PACKET_TYPE_VIDEO;
    packet->m_nChannel = 0x10;
    packet->m_headerType = RTMP_PACKET_SIZE_LARGE;
    //在使用完packet,要釋放。
    callback(packet);
}

如果在X264編碼過程中需要調試,可以通過指定日誌回調,來查看編碼過程:

//打開log輸出,查看編碼過程的日誌,這裏是指定日誌的回調。

//param.pf_log = x264_log_default2;

//打印x264編碼的異常輸出。得到整個編碼的過程日誌。
void x264_log_default2(void *, int i_level, const char *psz, va_list list) {
    __android_log_vprint(ANDROID_LOG_ERROR, "X264", psz, list);
}

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章