日日操夜夜添-日日操影院-日日草夜夜操-日日干干-精品一区二区三区波多野结衣-精品一区二区三区高清免费不卡

公告:魔扣目錄網為廣大站長提供免費收錄網站服務,提交前請做好本站友鏈:【 網站目錄:http://www.ylptlb.cn 】, 免友鏈快審服務(50元/站),

點擊這里在線咨詢客服
新站提交
  • 網站:51998
  • 待審:31
  • 小程序:12
  • 文章:1030137
  • 會員:747

要理解RTMP推流,我們就要知道詳細原理。

本文將詳細的來給大家介紹RTMP推流原理以及如何推送到服務器,首先我們了解一下推流的全過程:

 

我們將會分為幾個小節來展開:

一. 本文用到的庫文件:

1.1 本項目用到的庫文件如下圖所示,用到了ffmpeg庫,以及編碼視頻的x264,編碼音頻的fdk-aac,推流使用的rtmp等:

 


 

使用靜態鏈接庫,最終把這些.a文件打包到libstream中,Android.mk如下

LOCAL_PATH := $(call my-dir)

include $(CLEAR_VARS)
LOCAL_MODULE := avformat
LOCAL_SRC_FILES := $(LOCAL_PATH)/lib/libavformat.a
include $(PREBUILT_STATIC_LIBRARY)

include $(CLEAR_VARS)
LOCAL_MODULE := avcodec
LOCAL_SRC_FILES := $(LOCAL_PATH)/lib/libavcodec.a
include $(PREBUILT_STATIC_LIBRARY)

include $(CLEAR_VARS)
LOCAL_MODULE := swscale
LOCAL_SRC_FILES := $(LOCAL_PATH)/lib/libswscale.a
include $(PREBUILT_STATIC_LIBRARY)

include $(CLEAR_VARS)
LOCAL_MODULE := avutil
LOCAL_SRC_FILES := $(LOCAL_PATH)/lib/libavutil.a
include $(PREBUILT_STATIC_LIBRARY)

include $(CLEAR_VARS)
LOCAL_MODULE := swresample
LOCAL_SRC_FILES := $(LOCAL_PATH)/lib/libswresample.a
include $(PREBUILT_STATIC_LIBRARY)

include $(CLEAR_VARS)
LOCAL_MODULE := postproc
LOCAL_SRC_FILES := $(LOCAL_PATH)/lib/libpostproc.a
include $(PREBUILT_STATIC_LIBRARY)

include $(CLEAR_VARS)
LOCAL_MODULE := x264
LOCAL_SRC_FILES := $(LOCAL_PATH)/lib/libx264.a
include $(PREBUILT_STATIC_LIBRARY)

include $(CLEAR_VARS)
LOCAL_MODULE :=  libyuv
LOCAL_SRC_FILES := $(LOCAL_PATH)/lib/libyuv.a
include $(PREBUILT_STATIC_LIBRARY)

include $(CLEAR_VARS)
LOCAL_MODULE :=  libfdk-aac
#LOCAL_C_INCLUDES += $(LOCAL_PATH)/include/
LOCAL_SRC_FILES := $(LOCAL_PATH)/lib/libfdk-aac.a
include $(PREBUILT_STATIC_LIBRARY)

include $(CLEAR_VARS)
LOCAL_MODULE :=  polarssl
LOCAL_SRC_FILES := $(LOCAL_PATH)/lib/libpolarssl.a
include $(PREBUILT_STATIC_LIBRARY)

include $(CLEAR_VARS)
LOCAL_MODULE :=  rtmp
LOCAL_SRC_FILES := $(LOCAL_PATH)/lib/librtmp.a
include $(PREBUILT_STATIC_LIBRARY)

include $(CLEAR_VARS)
LOCAL_MODULE    := libstream

LOCAL_SRC_FILES := StreamProcess.cpp FrameEncoder.cpp AudioEncoder.cpp wavreader.c RtmpLivePublish.cpp
LOCAL_C_INCLUDES += $(LOCAL_PATH)/include/
LOCAL_STATIC_LIBRARIES := libyuv avformat avcodec swscale avutil swresample postproc x264 libfdk-aac polarssl rtmp
LOCAL_LDLIBS += -L$(LOCAL_PATH)/prebuilt/ -llog -lz -Ipthread

include $(BUILD_SHARED_LIBRARY)

具體使用到哪些庫中的接口我們將再下面進行細節展示。

 

二 . 如何從Camera攝像頭獲取視頻流:

2.1 Camera獲取視頻流,這個就不用多說了,只需要看到這個回調就行了,我們需要獲取到這個數據:

    //CameraSurfaceView.JAVA中
    @Override
    public void onPreviewFrame(byte[] data, Camera camera) {
        camera.addCallbackBuffer(data);
        if (listener != null) {
            listener.onCallback(data);
        }
    }
    //阻塞線程安全隊列,生產者和消費者
    private LinkedBlockingQueue<byte[]> mQueue = new LinkedBlockingQueue<>();
...........
@Override
    public void onCallback(final byte[] srcData) {
        if (srcData != null) {
            try {
                mQueue.put(srcData);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
.......

2.2 NV21轉化為YUV420P數據 我們知道一般的攝像頭的數據都是NV21或者是NV12,接下來我們會用到第一個編碼庫libyuv庫,我們先來看看這個消費者怎么從NV21的數據轉化為YUV的

    workThread = new Thread() {
            @Override
            public void run() {
                while (loop && !Thread.interrupted()) {
                    try {
                        //獲取阻塞隊列中的數據,沒有數據的時候阻塞
                        byte[] srcData = mQueue.take();
                        //生成I420(YUV標準格式數據及YUV420P)目標數據,
                        //生成后的數據長度width * height * 3 / 2
                        final byte[] dstData = new byte[scaleWidth * scaleHeight * 3 / 2];
                        final int morientation = mCameraUtil.getMorientation();
                        //壓縮NV21(YUV420SP)數據,元素數據位1080 * 1920,很顯然
                        //這樣的數據推流會很占用帶寬,我們壓縮成480 * 640 的YUV數據
                        //為啥要轉化為YUV420P數據?因為是在為轉化為H264數據在做
                        //準備,NV21不是標準的,只能先通過轉換,生成標準YUV420P數據,
                        //然后把標準數據encode為H264流
                        StreamProcessManager.compressYUV(srcData, mCameraUtil.getCameraWidth(), mCameraUtil.getCameraHeight(), dstData, scaleHeight, scaleWidth, 0, morientation, morientation == 270);

                        //進行YUV420P數據裁剪的操作,測試下這個借口,
                        //我們可以對數據進行裁剪,裁剪后的數據也是I420數據,
                        //我們采用的是libyuv庫文件
                        //這個libyuv庫效率非常高,這也是我們用它的原因
                        final byte[] cropData = new byte[cropWidth * cropHeight * 3 / 2];
                        StreamProcessManager.cropYUV(dstData, scaleWidth, scaleHeight, cropData, cropWidth, cropHeight, cropStartX, cropStartY);

                        //自此,我們得到了YUV420P標準數據,這個過程實際上就是NV21轉化為YUV420P數據
                        //注意,有些機器是NV12格式,只是數據存儲不一樣,我們一樣可以用libyuv庫的接口轉化
                        if (yuvDataListener != null) {
                            yuvDataListener.onYUVDataReceiver(cropData, cropWidth, cropHeight);
                        }

                        //設置為true,我們把生成的YUV文件用播放器播放一下,看我們
                        //的數據是否有誤,起調試作用
                        if (SAVE_FILE_FOR_TEST) {
                            fileManager.saveFileData(cropData);
                        }
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                        break;
                    }
                }
            }
        };

2.3 介紹一下攝像頭的數據流格式

視頻流的轉換,android中一般攝像頭的格式是NV21或者是NV12,它們都是YUV420sp的一種,那么什么是YUV格式呢?

何為YUV格式,有三個分量,Y表示明亮度,也就是灰度值,U和V則表示色度,即影像色彩飽和度,用于指定像素的顏色,(直接點就是Y是亮度信息,UV是色彩信息),YUV格式分為兩大類,planar和packed兩種:

對于planar的YUV格式,先連續存儲所有像素點Y,緊接著存儲所有像素點U,隨后所有像素點V
對于packed的YUV格式,每個像素點YUV是連續交替存儲的

YUV格式為什么后面還帶數字呢,比如YUV 420,444,442 YUV444:每一個Y對應一組UV分量 YUV422:每兩個Y共用一組UV分量 YUV420:每四個Y公用一組UV分量

實際上NV21,NV12就是屬于YUV420,是一種two-plane模式,即Y和UV分為兩個Plane,UV為交錯存儲,他們都屬于YUV420SP,舉個例子就會很清晰了

NV21格式數據排列方式是YYYYYYYY(w*h)VUVUVUVU(w*h/2),
對于NV12的格式,排列方式是YYYYYYYY(w*h)UVUVUVUV(w*h/2)

正如代碼注釋中所說的那樣,我們以標準的YUV420P為例,對于這樣的格式,我們要取出Y,U,V這三個分量,我們看怎么取?

比如480 * 640大小的圖片,其字節數為 480 * 640 * 3 >> 1個字節
Y分量:480 * 640個字節
U分量:480 * 640 >>2個字節
V分量:480 * 640 >>2個字節,加起來就為480 * 640 * 3 >> 1個字節
存儲都是行優先存儲,三部分之間順序是YUV依次存儲,即
0 ~ 480*640是Y分量;480 * 640 ~ 480 * 640 * 5 / 4為U分量;480 * 640 * 5 / 4 ~ 480 * 640 * 3 / 2是V分量,

記住這個計算方法,等下在JNI中馬上會體現出來

那么YUV420SP和YUV420P的區別在哪里呢?顯然Y的排序是完全相同的,但是UV排列上原理是完全不同的,420P它是先吧U存放完后,再放V,也就是說UV是連續的,而420SP它是UV,UV這樣交替存放: YUV420SP格式:

 

YUV420P格式:

 

所以NV21(YUV420SP)的數據如下: 同樣的以480 * 640大小的圖片為例,其字節數為 480 * 640 * 3 >> 1個字節 Y分量:480 * 640個字節 UV分量:480 * 640 >>1個字節(注意,我們沒有把UV分量分開) 加起來就為480 * 640 * 3 >> 1個字節

下面我們來看看兩個JNI函數,這個是攝像頭轉化的兩個最關鍵的函數

/**
     * NV21轉化為YUV420P數據
     * @param src         原始數據
     * @param width       原始數據寬度
     * @param height      原始數據高度
     * @param dst         生成數據
     * @param dst_width   生成數據寬度
     * @param dst_height  生成數據高度
     * @param mode        模式
     * @param degree      角度
     * @param isMirror    是否鏡像
     * @return
     */
    public static native int compressYUV(byte[] src, int width, int height, byte[] dst, int dst_width, int dst_height, int mode, int degree, boolean isMirror);

    /**
     * YUV420P數據的裁剪
     * @param src         原始數據
     * @param width       原始數據寬度
     * @param height      原始數據高度
     * @param dst         生成數據
     * @param dst_width   生成數據寬度
     * @param dst_height  生成數據高度
     * @param left        裁剪的起始x點
     * @param top         裁剪的起始y點
     * @return
     */
    public static native int cropYUV(byte[] src, int width, int height, byte[] dst, int dst_width, int dst_height, int left, int top);

再看一看具體實現

JNIEXPORT jint JNICALL Java_com_riemannlee_liveproject_StreamProcessManager_compressYUV
    (JNIEnv *env, jclass type,
     jbyteArray src_, jint width,
     jint height, jbyteArray dst_,
     jint dst_width, jint dst_height,
     jint mode, jint degree,
     jboolean isMirror) {

    jbyte *Src_data = env->GetByteArrayElements(src_, NULL);
    jbyte *Dst_data = env->GetByteArrayElements(dst_, NULL);
    //nv21轉化為i420(標準YUV420P數據) 這個temp_i420_data大小是和Src_data是一樣的
    nv21ToI420(Src_data, width, height, temp_i420_data);
    //進行縮放的操作,這個縮放,會把數據壓縮
    scaleI420(temp_i420_data, width, height, temp_i420_data_scale, dst_width, dst_height, mode);
    //如果是前置攝像頭,進行鏡像操作
    if (isMirror) {
        //進行旋轉的操作
        rotateI420(temp_i420_data_scale, dst_width, dst_height, temp_i420_data_rotate, degree);
        //因為旋轉的角度都是90和270,那后面的數據width和height是相反的
        mirrorI420(temp_i420_data_rotate, dst_height, dst_width, Dst_data);
    } else {
        //進行旋轉的操作
        rotateI420(temp_i420_data_scale, dst_width, dst_height, Dst_data, degree);
    }
    env->ReleaseByteArrayElements(dst_, Dst_data, 0);
    env->ReleaseByteArrayElements(src_, Src_data, 0);

    return 0;
}

我們從java層傳遞過來的參數可以看到,原始數據是1080 * 1920,先轉為1080 * 1920的標準的YUV420P的數據,下面的代碼就是上面我舉的例子,如何拆分YUV420P的Y,U,V分量和如何拆分YUV420SP的Y,UV分量,最后調用libyuv庫的libyuv::NV21ToI420數據就完成了轉換;然后進行縮放,調用了libyuv::I420Scale的函數完成轉換

//NV21轉化為YUV420P數據
void nv21ToI420(jbyte *src_nv21_data, jint width, jint height, jbyte *src_i420_data) {
    //Y通道數據大小
    jint src_y_size = width * height;
    //U通道數據大小
    jint src_u_size = (width >> 1) * (height >> 1);

    //NV21中Y通道數據
    jbyte *src_nv21_y_data = src_nv21_data;
    //由于是連續存儲的Y通道數據后即為VU數據,它們的存儲方式是交叉存儲的
    jbyte *src_nv21_vu_data = src_nv21_data + src_y_size;

    //YUV420P中Y通道數據
    jbyte *src_i420_y_data = src_i420_data;
    //YUV420P中U通道數據
    jbyte *src_i420_u_data = src_i420_data + src_y_size;
    //YUV420P中V通道數據
    jbyte *src_i420_v_data = src_i420_data + src_y_size + src_u_size;

    //直接調用libyuv中接口,把NV21數據轉化為YUV420P標準數據,此時,它們的存儲大小是不變的
    libyuv::NV21ToI420((const uint8 *) src_nv21_y_data, width,
                       (const uint8 *) src_nv21_vu_data, width,
                       (uint8 *) src_i420_y_data, width,
                       (uint8 *) src_i420_u_data, width >> 1,
                       (uint8 *) src_i420_v_data, width >> 1,
                       width, height);
}

//進行縮放操作,此時是把1080 * 1920的YUV420P的數據 ==> 480 * 640的YUV420P的數據
void scaleI420(jbyte *src_i420_data, jint width, jint height, jbyte *dst_i420_data, jint dst_width,
               jint dst_height, jint mode) {
    //Y數據大小width*height,U數據大小為1/4的width*height,V大小和U一樣,一共是3/2的width*height大小
    jint src_i420_y_size = width * height;
    jint src_i420_u_size = (width >> 1) * (height >> 1);
    //由于是標準的YUV420P的數據,我們可以把三個通道全部分離出來
    jbyte *src_i420_y_data = src_i420_data;
    jbyte *src_i420_u_data = src_i420_data + src_i420_y_size;
    jbyte *src_i420_v_data = src_i420_data + src_i420_y_size + src_i420_u_size;

    //由于是標準的YUV420P的數據,我們可以把三個通道全部分離出來
    jint dst_i420_y_size = dst_width * dst_height;
    jint dst_i420_u_size = (dst_width >> 1) * (dst_height >> 1);
    jbyte *dst_i420_y_data = dst_i420_data;
    jbyte *dst_i420_u_data = dst_i420_data + dst_i420_y_size;
    jbyte *dst_i420_v_data = dst_i420_data + dst_i420_y_size + dst_i420_u_size;

    //調用libyuv庫,進行縮放操作
    libyuv::I420Scale((const uint8 *) src_i420_y_data, width,
                      (const uint8 *) src_i420_u_data, width >> 1,
                      (const uint8 *) src_i420_v_data, width >> 1,
                      width, height,
                      (uint8 *) dst_i420_y_data, dst_width,
                      (uint8 *) dst_i420_u_data, dst_width >> 1,
                      (uint8 *) dst_i420_v_data, dst_width >> 1,
                      dst_width, dst_height,
                      (libyuv::FilterMode) mode);
}

至此,我們就把攝像頭的NV21數據轉化為YUV420P的標準數據了,這樣,我們就可以把這個數據流轉化為H264了,接下來,我們來看看如何把YUV420P流數據轉化為h264數據,從而為推流做準備

三 標準YUV420P數據編碼為H264

多說無用,直接上代碼

3.1 代碼如何實現h264編碼的:

/**
 * 編碼類MediaEncoder,主要是把視頻流YUV420P格式編碼為h264格式,把PCM裸音頻轉化為AAC格式
 */
public class MediaEncoder {
    private static final String TAG = "MediaEncoder";

    private Thread videoEncoderThread, audioEncoderThread;
    private boolean videoEncoderLoop, audioEncoderLoop;

    //視頻流隊列
    private LinkedBlockingQueue<VideoData> videoQueue;
    //音頻流隊列
    private LinkedBlockingQueue<AudioData> audioQueue;
.........

//攝像頭的YUV420P數據,put到隊列中,生產者模型
    public void putVideoData(VideoData videoData) {
        try {
            videoQueue.put(videoData);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
.........
 videoEncoderThread = new Thread() {
            @Override
            public void run() {
                //視頻消費者模型,不斷從隊列中取出視頻流來進行h264編碼
                while (videoEncoderLoop && !Thread.interrupted()) {
                    try {
                        //隊列中取視頻數據
                        VideoData videoData = videoQueue.take();
                        fps++;
                        byte[] outbuffer = new byte[videoData.width * videoData.height];
                        int[] buffLength = new int[10];
                        //對YUV420P進行h264編碼,返回一個數據大小,里面是編碼出來的h264數據
                        int numNals = StreamProcessManager.encoderVideoEncode(videoData.videoData, videoData.videoData.length, fps, outbuffer, buffLength);
                        //Log.e("RiemannLee", "data.length " +  videoData.videoData.length + " h264 encode length " + buffLength[0]);
                        if (numNals > 0) {
                            int[] segment = new int[numNals];
                            System.arraycopy(buffLength, 0, segment, 0, numNals);
                            int totalLength = 0;
                            for (int i = 0; i < segment.length; i++) {
                                totalLength += segment[i];
                            }
                            //Log.i("RiemannLee", "###############totalLength " + totalLength);
                            //編碼后的h264數據
                            byte[] encodeData = new byte[totalLength];
                            System.arraycopy(outbuffer, 0, encodeData, 0, encodeData.length);
                            if (sMediaEncoderCallback != null) {
                                sMediaEncoderCallback.receiveEncoderVideoData(encodeData, encodeData.length, segment);
                            }
                            //我們可以把數據在java層保存到文件中,看看我們編碼的h264數據是否能播放,h264裸數據可以在VLC播放器中播放
                            if (SAVE_FILE_FOR_TEST) {
                                videoFileManager.saveFileData(encodeData);
                            }
                        }
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                        break;
                    }
                }

            }
        };
        videoEncoderLoop = true;
        videoEncoderThread.start();
    }

至此,我們就把攝像頭的NV21數據轉化為YUV420P的標準數據了,這樣,我們就可以把這個數據流轉化為H264了,接下來,我們來看看如何把YUV420P流數據轉化為h264數據,從而為推流做準備

三 標準YUV420P數據編碼為H264

多說無用,直接上代碼

3.1 代碼如何實現h264編碼的:

/**
 * 編碼類MediaEncoder,主要是把視頻流YUV420P格式編碼為h264格式,把PCM裸音頻轉化為AAC格式
 */
public class MediaEncoder {
    private static final String TAG = "MediaEncoder";

    private Thread videoEncoderThread, audioEncoderThread;
    private boolean videoEncoderLoop, audioEncoderLoop;

    //視頻流隊列
    private LinkedBlockingQueue<VideoData> videoQueue;
    //音頻流隊列
    private LinkedBlockingQueue<AudioData> audioQueue;
.........

//攝像頭的YUV420P數據,put到隊列中,生產者模型
    public void putVideoData(VideoData videoData) {
        try {
            videoQueue.put(videoData);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
.........
 videoEncoderThread = new Thread() {
            @Override
            public void run() {
                //視頻消費者模型,不斷從隊列中取出視頻流來進行h264編碼
                while (videoEncoderLoop && !Thread.interrupted()) {
                    try {
                        //隊列中取視頻數據
                        VideoData videoData = videoQueue.take();
                        fps++;
                        byte[] outbuffer = new byte[videoData.width * videoData.height];
                        int[] buffLength = new int[10];
                        //對YUV420P進行h264編碼,返回一個數據大小,里面是編碼出來的h264數據
                        int numNals = StreamProcessManager.encoderVideoEncode(videoData.videoData, videoData.videoData.length, fps, outbuffer, buffLength);
                        //Log.e("RiemannLee", "data.length " +  videoData.videoData.length + " h264 encode length " + buffLength[0]);
                        if (numNals > 0) {
                            int[] segment = new int[numNals];
                            System.arraycopy(buffLength, 0, segment, 0, numNals);
                            int totalLength = 0;
                            for (int i = 0; i < segment.length; i++) {
                                totalLength += segment[i];
                            }
                            //Log.i("RiemannLee", "###############totalLength " + totalLength);
                            //編碼后的h264數據
                            byte[] encodeData = new byte[totalLength];
                            System.arraycopy(outbuffer, 0, encodeData, 0, encodeData.length);
                            if (sMediaEncoderCallback != null) {
                                sMediaEncoderCallback.receiveEncoderVideoData(encodeData, encodeData.length, segment);
                            }
                            //我們可以把數據在java層保存到文件中,看看我們編碼的h264數據是否能播放,h264裸數據可以在VLC播放器中播放
                            if (SAVE_FILE_FOR_TEST) {
                                videoFileManager.saveFileData(encodeData);
                            }
                        }
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                        break;
                    }
                }

            }
        };
        videoEncoderLoop = true;
        videoEncoderThread.start();
    }

這個就是如何把YUV420P數據轉化為h264流,主要代碼是這個JNI函數,接下來我們看是如何編碼成h264的,編碼函數如下:

    /**
     * 編碼視頻數據接口
     * @param srcFrame      原始數據(YUV420P數據)
     * @param frameSize     幀大小
     * @param fps           fps
     * @param dstFrame      編碼后的數據存儲
     * @param outFramewSize 編碼后的數據大小
     * @return
     */
    public static native int encoderVideoEncode(byte[] srcFrame, int frameSize, int fps, byte[] dstFrame, int[] outFramewSize);

JNI中視頻流的編碼接口,我們看到的是初始化一個FrameEncoder類,然后調用這個類的encodeFrame接口去編碼

//初始化視頻編碼
JNIEXPORT jint JNICALL Java_com_riemannlee_liveproject_StreamProcessManager_encoderVideoinit
        (JNIEnv *env, jclass type, jint jwidth, jint jheight, jint joutwidth, jint joutheight)
{
    frameEncoder = new FrameEncoder();
    frameEncoder->setInWidth(jwidth);
    frameEncoder->setInHeight(jheight);
    frameEncoder->setOutWidth(joutwidth);
    frameEncoder->setOutHeight(joutheight);
    frameEncoder->setBitrate(128);
    frameEncoder->open();
    return 0;
}

//視頻編碼主要函數,注意JNI函數GetByteArrayElements和ReleaseByteArrayElements成對出現,否則回內存泄露
JNIEXPORT jint JNICALL Java_com_riemannlee_liveproject_StreamProcessManager_encoderVideoEncode
        (JNIEnv *env, jclass type, jbyteArray jsrcFrame, jint jframeSize, jint counter, jbyteArray jdstFrame, jintArray jdstFrameSize)
{
    jbyte *Src_data = env->GetByteArrayElements(jsrcFrame, NULL);
    jbyte *Dst_data = env->GetByteArrayElements(jdstFrame, NULL);
    jint *dstFrameSize = env->GetIntArrayElements(jdstFrameSize, NULL);

    int numNals = frameEncoder->encodeFrame((char*)Src_data, jframeSize, counter, (char*)Dst_data, dstFrameSize);

    env->ReleaseByteArrayElements(jdstFrame, Dst_data, 0);
    env->ReleaseByteArrayElements(jsrcFrame, Src_data, 0);
    env->ReleaseIntArrayElements(jdstFrameSize, dstFrameSize, 0);

    return numNals;
}

下面我們來詳細的分析FrameEncoder這個C++類,這里我們用到了多個庫,第一個就是鼎鼎大名的ffmpeg庫,還有就是X264庫,下面我們先來了解一下h264的文件結構,這樣有利于我們理解h264的編碼流程

3.2 h264我們必須知道的一些概念:

首先我們來介紹h264字節流,先來了解下面幾個概念,h264由哪些東西組成呢?
1.VCL   video coding layer    視頻編碼層;
2.NAL  .NETwork abstraction layer   網絡提取層;
其中,VCL層是對核心算法引擎,塊,宏塊及片的語法級別的定義,他最終輸出編碼完的數據 SODB

SODB:String of Data Bits,數據比特串,它是最原始的編碼數據
RBSP:Raw Byte Sequence Payload,原始字節序載荷,它是在SODB的后面添加了結尾比特和若干比特0,以便字節對齊
EBSP:Encapsulate Byte Sequence Payload,擴展字節序列載荷,它是在RBSP基礎上添加了防校驗字節0x03后得到的。
關系大致如下:

SODB + RBSP STOP bit + 0bits = RBSP

RBSP part1+0x03+RBSP part2+0x03+…+RBSP partn = EBSP

NALU Header+EBSP=NALU(NAL單元)

start code+NALU+…+start code+NALU=H.264 Byte Stream

NALU頭結構

長度:1byte(1個字節)
forbidden_bit(1bit) + nal_reference_bit(2bit) + nal_unit_type(5bit)
1.  forbidden_bit:禁止位,初始為0,當網絡發現NAL單元有比特錯誤時可設置該比特為1,以便接收方糾錯或
丟掉該單元。
2.  nal_reference_bit:nal重要性指示,標志該NAL單元的重要性,值越大,越重要,解碼器在解碼處理不過來
的時候,可以丟掉重要性為0的NALU。

NALU類型結構圖:

 

其中,nal_unit_type為1, 2, 3, 4, 5及12的NAL單元稱為VCL的NAL單元,其他類型的NAL單元為非VCL的NAL單元。

對應的代碼定義如下

    public static final int NAL_UNKNOWN     = 0;
    public static final int NAL_SLICE       = 1; /* 非關鍵幀 */
    public static final int NAL_SLICE_DPA   = 2;
    public static final int NAL_SLICE_DPB   = 3;
    public static final int NAL_SLICE_DPC   = 4;
    public static final int NAL_SLICE_IDR   = 5; /* 關鍵幀 */
    public static final int NAL_SEI         = 6;
    public static final int NAL_SPS         = 7; /* SPS */
    public static final int NAL_PPS         = 8; /* PPS */
    public static final int NAL_AUD         = 9;
    public static final int NAL_FILLER      = 12;

由上面我們可以知道,h264字節流,就是由一些start code + NALU組成的,要組成一個NALU單元,首先要有原始數據,稱之為SODB,它是原始的H264數據編碼得到到,不包括3字節(0x000001)/4字節(0x00000001)的start code,也不會包括1字節的NALU頭, NALU頭部信息包括了一些基礎信息,比如NALU類型。 ps:起始碼包括兩種,3字節0x000001和4字節0x00000001,在sps和pps和Access Unit的第一個NALU使用4字節起始碼,其余情況均使用3字節起始碼

在 H264 SPEC 中,RBSP 定義如下: 在SODB結束處添加表示結束的bit 1來表示SODB已經結束,因此添加的bit 1成為rbsp_stop_one_bit,RBSP也需要字節對齊,為此需要在rbsp_stop_one_bit后添加若干0補齊,簡單來說,要在SODB后面追加兩樣東西就形成了RBSP rbsp_stop_one_bit = 1 rbsp_alignment_zero_bit(s) = 0(s)

RBSP的生成過程:

 

即RBSP最后一個字節包含SODB最后幾個比特,以及trailing bits其中,第一個比特位1,其余的比特位0,保證字節對齊,最后再結尾處添加0x0000,即CABAC_ZERO_word,從而形成 RBSP。

EBSP的生成過程:NALU數據+起始碼就形成了AnnexB格式(下面有介紹H264的兩種格式,AnnexB為常用的格式),起始碼包括兩種,0x000001和0x00000001,為了不讓NALU的主體和起始碼之間產生競爭,在對RBSP進行掃描的時候,如果遇到連續兩個0x00字節,則在該兩個字節后面添加一個0x03字節,在解碼的時候將該0x03字節去掉,也稱為脫殼操作。解碼器在解碼時,首先逐個字節讀取NAL的數據,統計NAL的長度,然后再開始解碼。 替換規則如下: 0x000000 => 0x00000300 0x000001 => 0x00000301 0x000002 => 0x00000302 0x000003 => 0x00000303

3.3 下面我們找一個h264文件來看看

 

00 00 00 01 67 ... 這個為SPS,67為NALU Header,有type信息,后面即為我們說的EBSP
00 00 00 01 68 ...  這個為PPS
00 00 01 06 ...  為SEI補充增強信息
00 00 01 65...  為IDR關鍵幀,圖像中的編碼slice

 

對于這個SPS集合,從67type后開始計算, 即42 c0 33 a6 80 b4 1e 68 40 00 00 03 00 40 00 00 0c a3 c6 0c a8 正如前面的描述,解碼的時候直接03 這個03是競爭檢測

 

從前面我們分析知道,VCL層出來的是編碼完的視頻幀數據,這些幀可能是I,B,P幀,而且這些幀可能屬于不同的序列,在這同一個序列還有相對應的一套序列參數集和圖片參數集,所以要完成視頻的解碼,不僅需要傳輸VCL層編碼出來的視頻幀數據,還需要傳輸序列參數集,圖像參數集等數據。

參數集:包括序列參數集SPS和圖像參數集PPS

SPS:包含的是針對一連續編碼視頻序列的參數,如標識符seq_parameter_set_id,幀數以及POC的約束,參數幀數目,解碼圖像尺寸和幀場編碼模式選擇標識等等 PPS:對應的是一個序列中某一副圖像或者某幾幅圖像,其參數如標識符pic_parameter_set_id、可選的 seq_parameter_set_id、熵編碼模式選擇標識,片組數目,初始量化參數和去方塊濾波系數調整標識等等 數據分割:組成片的編碼數據存放在3個獨立的DP(數據分割A,B,C)中,各自包含一個編碼片的子集, 分割A包含片頭和片中宏塊頭數據 分割B包含幀內和 SI 片宏塊的編碼殘差數據。 分割 C包含幀間宏塊的編碼殘差數據。 每個分割可放在獨立的 NAL 單元并獨立傳輸。

NALU的順序要求 H264/AVC標準對送到解碼器的NAL單元是由嚴格要求的,如果NAL單元的順序是混亂的,必須將其重新依照規范組織后送入解碼器,否則不能正確解碼

1.  序列參數集NAL單元
必須在傳送所有以此參數集為參考的其它NAL單元之前傳送,不過允許這些NAL單元中中間出現重復的序列參數集合NAL單元。
所謂重復的詳細解釋為:序列參數集NAL單元都有其專門的標識,如果兩個序列參數集NAL單元的標識相同,就可以認為后一個只不過是前一個的拷貝,而非新的序列參數集
2.  圖像參數集NAL單元
必須在所有此參數集為參考的其它NAL單元之前傳送,不過允許這些NAL單元中間出現重復的圖像參數集NAL單元,這一點與上述的序列參數集NAL單元是相同的。
3.  不同基本編碼圖像中的片段(slice)單元和數據劃分片段(data partition)單元在順序上不可以相互交叉,即不允許屬于某一基本編碼圖像的一系列片段(slice)單元和數據劃分片段(data partition)單元中忽然出現另一個基本編碼圖像的片段(slice)單元片段和數據劃分片段(data partition)單元。
4.  參考圖像的影響:如果一幅圖像以另一幅圖像為參考,則屬于前者的所有片段(slice)單元和數據劃分片段(data partition)單元必須在屬于后者的片段和數據劃分片段之后,無論是基本編碼圖像還是冗余編碼圖像都必須遵守這個規則。
5.  基本編碼圖像的所有片段(slice)單元和數據劃分片段(data partition)單元必須在屬于相應冗余編碼圖像的片段(slice)單元和數據劃分片段(data partition)單元之前。
6.  如果數據流中出現了連續的無參考基本編碼圖像,則圖像序號小的在前面。
7.  如果arbitrary_slice_order_allowed_flag置為1,一個基本編碼圖像中的片段(slice)單元和數據劃分片段(data partition)單元的順序是任意的,如果arbitrary_slice_order_allowed_flag置為零,則要按照片段中第一個宏塊的位置來確定片段的順序,若使用數據劃分,則A類數據劃分片段在B類數據劃分片段之前,B類數據劃分片段在C類數據劃分片段之前,而且對應不同片段的數據劃分片段不能相互交叉,也不能與沒有數據劃分的片段相互交叉。
8.  如果存在SEI(補充增強信息)單元的話,它必須在它所對應的基本編碼圖像的片段(slice)單元和數據劃分片段(data partition)單元之前,并同時必須緊接在上一個基本編碼圖像的所有片段(slice)單元和數據劃分片段(data partition)單元后邊。假如SEI屬于多個基本編碼圖像,其順序僅以第一個基本編碼圖像為參照。
9.  如果存在圖像分割符的話,它必須在所有SEI 單元、基本編碼圖像的所有片段slice)單元和數據劃分片段(data partition)單元之前,并且緊接著上一個基本編碼圖像那些NAL單元。
10.  如果存在序列結束符,且序列結束符后還有圖像,則該圖像必須是IDR(即時解碼器刷新)圖像。序列結束符的位置應當在屬于這個IDR圖像的分割符、SEI 單元等數據之前,且緊接著前面那些圖像的NAL單元。如果序列結束符后沒有圖像了,那么它的就在比特流中所有圖像數據之后。
11.  流結束符在比特流中的最后。

h264有兩種封裝, 一種是Annexb模式,傳統模式,有startcode,SPS和PPS是在ES中 一種是mp4模式,一般mp4 mkv會有,沒有startcode,SPS和PPS以及其它信息被封裝在container中,每一個frame前面是這個frame的長度 很多解碼器只支持annexb這種模式,因此需要將mp4做轉換 我們討論的是第一種Annexb傳統模式,

3.4 下面我們直接看代碼,了解一下如何使用X264來編碼h264文件

x264_param_default_preset():為了方便使用x264,只需要根據編碼速度的要求和視頻質量的要求選擇模型,
并修改部分視頻參數即可
x264_picture_alloc():為圖像結構體x264_picture_t分配內存。
x264_encoder_open():打開編碼器。
x264_encoder_encode():編碼一幀圖像。
x264_encoder_close():關閉編碼器。
x264_picture_clean():釋放x264_picture_alloc()申請的資源。
 
存儲數據的結構體如下所示。
x264_picture_t:存儲壓縮編碼前的像素數據。
x264_nal_t:存儲壓縮編碼后的碼流數據。

下面介紹幾個重要的結構體
/********************************************************************************************
 x264_image_t 結構用于存放一幀圖像實際像素數據。該結構體定義在x264.h中
*********************************************************************************************/
typedef struct
{
    int     i_csp;          // 設置彩色空間,通常取值 X264_CSP_I420,所有可能取值定義在x264.h中
    int     i_plane;        // 圖像平面個數,例如彩色空間是YUV420格式的,此處取值3
    int     i_stride[4];    // 每個圖像平面的跨度,也就是每一行數據的字節數
    uint8_t *plane[4];      // 每個圖像平面存放數據的起始地址, plane[0]是Y平面,
                            // plane[1]和plane[2]分別代表U和V平面
}  x264_image_t;

/********************************************************************************************
x264_picture_t 結構體描述視頻幀的特征,該結構體定義在x264.h中。
*********************************************************************************************/
typedef struct
{
int   i_type;           // 幀的類型,取值有X264_TYPE_KEYFRAME X264_TYPE_P
                        // X264_TYPE_AUTO等。初始化為auto,則在編碼過程自行控制。
int   i_qpplus1;        // 此參數減1代表當前幀的量化參數值
int   i_pic_struct;     // 幀的結構類型,表示是幀還是場,是逐行還是隔行,
                        // 取值為枚舉值 pic_struct_e,定義在x264.h中
int   b_keyframe;       // 輸出:是否是關鍵幀
int64_t   i_pts;        // 一幀的顯示時間戳
int64_t   i_dts;        // 輸出:解碼時間戳。當一幀的pts非常接近0時,該dts值可能為負。

/* 編碼器參數設置,如果為NULL則表示繼續使用前一幀的設置。某些參數
   (例如aspect ratio) 由于收到H264本身的限制,只能每隔一個GOP才能改變。
   這種情況下,如果想讓這些改變的參數立即生效,則必須強制生成一個IDR幀。*/ 
x264_param_t    *param;

x264_image_t     img;    // 存放一幀圖像的真實數據
x264_image_properties_t    prop;
x264_hrd_t    hrd_timing;// 輸出:HRD時間信息,僅當i_nal_hrd設置了才有效
void    *opaque;         // 私有數據存放區,將輸入數據拷貝到輸出幀中
} x264_picture_t ;

/****************************************************************************************************************
x264_nal_t中的數據在下一次調用x264_encoder_encode之后就無效了,因此必須在調用
x264_encoder_encode 或 x264_encoder_headers 之前使用或拷貝其中的數據。
*****************************************************************************************************************/
typedef struct
{
int  i_ref_idc;        // Nal的優先級
int  i_type;           // Nal的類型
int  b_long_startcode; // 是否采用長前綴碼0x00000001
int  i_first_mb;       // 如果Nal為一條帶,則表示該條帶第一個宏塊的指數
int  i_last_mb;        // 如果Nal為一條帶,則表示該條帶最后一個宏塊的指數
int  i_payload;        // payload 的字節大小
uint8_t *p_payload;    // 存放編碼后的數據,已經封裝成Nal單元
} x264_nal_t;


再來看看編碼h264源碼

//初始化視頻編碼
JNIEXPORT jint JNICALL Java_com_riemannlee_liveproject_StreamProcessManager_encoderVideoinit
        (JNIEnv *env, jclass type, jint jwidth, jint jheight, jint joutwidth, jint joutheight)
{
    frameEncoder = new FrameEncoder();
    frameEncoder->setInWidth(jwidth);
    frameEncoder->setInHeight(jheight);
    frameEncoder->setOutWidth(joutwidth);
    frameEncoder->setOutHeight(joutheight);
    frameEncoder->setBitrate(128);
    frameEncoder->open();
    return 0;
}

FrameEncoder.cpp 源文件

//供測試文件使用,測試的時候打開
//#define ENCODE_OUT_FILE_1
//供測試文件使用
//#define ENCODE_OUT_FILE_2

FrameEncoder::FrameEncoder() : in_width(0), in_height(0), out_width(
        0), out_height(0), fps(0), encoder(NULL), num_nals(0) {

#ifdef ENCODE_OUT_FILE_1
    const char *outfile1 = "/sdcard/2222.h264";
    out1 = fopen(outfile1, "wb");
#endif

#ifdef ENCODE_OUT_FILE_2
    const char *outfile2 = "/sdcard/3333.h264";
    out2 = fopen(outfile2, "wb");
#endif
}

bool FrameEncoder::open() {
    int r = 0;
    int nheader = 0;
    int header_size = 0;

    if (!validateSettings()) {
        return false;
    }

    if (encoder) {
        LOGI("Already opened. first call close()");
        return false;
    }

    // set encoder parameters
    setParams();
    //按照色度空間分配內存,即為圖像結構體x264_picture_t分配內存,并返回內存的首地址作為指針
    //i_csp(圖像顏色空間參數,目前只支持I420/YUV420)為X264_CSP_I420
    x264_picture_alloc(&pic_in, params.i_csp, params.i_width, params.i_height);
    //create the encoder using our params 打開編碼器
    encoder = x264_encoder_open(¶ms);

    if (!encoder) {
        LOGI("Cannot open the encoder");
        close();
        return false;
    }

    // write headers
    r = x264_encoder_headers(encoder, &nals, &nheader);
    if (r < 0) {
        LOGI("x264_encoder_headers() failed");
        return false;
    }

    return true;
}
//編碼h264幀
int FrameEncoder::encodeFrame(char* inBytes, int frameSize, int pts,
                               char* outBytes, int *outFrameSize) {
    //YUV420P數據轉化為h264
    int i420_y_size = in_width * in_height;
    int i420_u_size = (in_width >> 1) * (in_height >> 1);
    int i420_v_size = i420_u_size;

    uint8_t *i420_y_data = (uint8_t *)inBytes;
    uint8_t *i420_u_data = (uint8_t *)inBytes + i420_y_size;
    uint8_t *i420_v_data = (uint8_t *)inBytes + i420_y_size + i420_u_size;
    //將Y,U,V數據保存到pic_in.img的對應的分量中,還有一種方法是用AV_fillPicture和sws_scale來進行變換
    memcpy(pic_in.img.plane[0], i420_y_data, i420_y_size);
    memcpy(pic_in.img.plane[1], i420_u_data, i420_u_size);
    memcpy(pic_in.img.plane[2], i420_v_data, i420_v_size);

    // and encode and store into pic_out
    pic_in.i_pts = pts;
    //最主要的函數,x264編碼,pic_in為x264輸入,pic_out為x264輸出
    int frame_size = x264_encoder_encode(encoder, &nals, &num_nals, &pic_in,
                                         &pic_out);

    if (frame_size) {
        /*Here first four bytes proceeding the nal unit indicates frame length*/
        int have_copy = 0;
        //編碼后,h264數據保存為nal了,我們可以獲取到nals[i].type的類型判斷是sps還是pps
        //或者是否是關鍵幀,nals[i].i_payload表示數據長度,nals[i].p_payload表示存儲的數據
        //編碼后,我們按照nals[i].i_payload的長度來保存copy h264數據的,然后拋給java端用作
        //rtmp發送數據,outFrameSize是變長的,當有sps pps的時候大于1,其它時候值為1
        for (int i = 0; i < num_nals; i++) {
            outFrameSize[i] = nals[i].i_payload;
            memcpy(outBytes + have_copy, nals[i].p_payload, nals[i].i_payload);
            have_copy += nals[i].i_payload;
        }
#ifdef ENCODE_OUT_FILE_1
        fwrite(outBytes, 1, frame_size, out1);
#endif

#ifdef ENCODE_OUT_FILE_2
        for (int i = 0; i < frame_size; i++) {
            outBytes[i] = (char) nals[0].p_payload[i];
        }
        fwrite(outBytes, 1, frame_size, out2);
        *outFrameSize = frame_size;
#endif

        return num_nals;
    }
    return -1;
}

最后,我們來看看拋往java層的h264數據,在MediaEncoder.java中,函數startVideoEncode:

public void startVideoEncode() {
        if (videoEncoderLoop) {
            throw new RuntimeException("必須先停止");
        }

        videoEncoderThread = new Thread() {
            @Override
            public void run() {
                //視頻消費者模型,不斷從隊列中取出視頻流來進行h264編碼
                while (videoEncoderLoop && !Thread.interrupted()) {
                    try {
                        //隊列中取視頻數據
                        VideoData videoData = videoQueue.take();
                        fps++;
                        byte[] outbuffer = new byte[videoData.width * videoData.height];
                        int[] buffLength = new int[10];
                        //對YUV420P進行h264編碼,返回一個數據大小,里面是編碼出來的h264數據
                        int numNals = StreamProcessManager.encoderVideoEncode(videoData.videoData, videoData.videoData.length, fps, outbuffer, buffLength);
                        //Log.e("RiemannLee", "data.length " +  videoData.videoData.length + " h264 encode length " + buffLength[0]);
                        if (numNals > 0) {
                            int[] segment = new int[numNals];
                            System.arraycopy(buffLength, 0, segment, 0, numNals);
                            int totalLength = 0;
                            for (int i = 0; i < segment.length; i++) {
                                totalLength += segment[i];
                            }
                            //Log.i("RiemannLee", "###############totalLength " + totalLength);
                            //編碼后的h264數據
                            byte[] encodeData = new byte[totalLength];
                            System.arraycopy(outbuffer, 0, encodeData, 0, encodeData.length);
                            if (sMediaEncoderCallback != null) {
                                sMediaEncoderCallback.receiveEncoderVideoData(encodeData, encodeData.length, segment);
                            }
                            //我們可以把數據在java層保存到文件中,看看我們編碼的h264數據是否能播放,h264裸數據可以在VLC播放器中播放
                            if (SAVE_FILE_FOR_TEST) {
                                videoFileManager.saveFileData(encodeData);
                            }
                        }
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                        break;
                    }
                }

            }
        };
        videoEncoderLoop = true;
        videoEncoderThread.start();
    }

此時,h264數據已經出來了,我們就實現了YUV420P的數據到H264數據的編碼,接下來,我們再來看看音頻數據。

3.5 android音頻數據如何使用fdk-aac庫來編碼音頻,轉化為AAC數據的,直接上代碼

public class AudioRecoderManager {

    private static final String TAG = "AudioRecoderManager";

    // 音頻獲取
    private final static int SOURCE = MediaRecorder.AudIOSource.MIC;

    // 設置音頻采樣率,44100是目前的標準,但是某些設備仍然支 2050 6000 1025
    private final static int SAMPLE_HZ = 44100;

    // 設置音頻的錄制的聲道CHANNEL_IN_STEREO為雙聲道,CHANNEL_CONFIGURATION_MONO為單聲道
    private final static int CHANNEL_CONFIG = AudioFormat.CHANNEL_IN_STEREO;

    // 音頻數據格式:PCM 16位每個樣本保證設備支持。PCM 8位每個樣本 不一定能得到設備支持
    private final static int FORMAT = AudioFormat.ENCODING_PCM_16BIT;

    private int mBufferSize;

    private AudioRecord mAudioRecord = null;
    private int bufferSizeInBytes = 0;
............

    public AudioRecoderManager() {
        if (SAVE_FILE_FOR_TEST) {
            fileManager = new FileManager(FileManager.TEST_PCM_FILE);
        }

        bufferSizeInBytes = AudioRecord.getMinBufferSize(SAMPLE_HZ, CHANNEL_CONFIG, FORMAT);
        mAudioRecord = new AudioRecord(SOURCE, SAMPLE_HZ, CHANNEL_CONFIG, FORMAT, bufferSizeInBytes);
        mBufferSize = 4 * 1024;
    }

        public void startAudioIn() {

        workThread = new Thread() {
            @Override
            public void run() {
                mAudioRecord.startRecording();
                byte[] audioData = new byte[mBufferSize];
                int readsize = 0;
                //錄音,獲取PCM裸音頻,這個音頻數據文件很大,我們必須編碼成AAC,這樣才能rtmp傳輸
                while (loop && !Thread.interrupted()) {
                    try {
                        readsize += mAudioRecord.read(audioData, readsize, mBufferSize);
                        byte[] ralAudio = new byte[readsize];
                        //每次錄音讀取4K數據
                        System.arraycopy(audioData, 0, ralAudio, 0, readsize);
                        if (audioDataListener != null) {
                            //把錄音的數據拋給MediaEncoder去編碼AAC音頻數據
                            audioDataListener.audioData(ralAudio);
                        }
                        //我們可以把裸音頻以文件格式存起來,判斷這個音頻是否是好的,只需要加一個WAV頭
                        //即形成WAV無損音頻格式
                        if (SAVE_FILE_FOR_TEST) {
                            fileManager.saveFileData(ralAudio);
                        }

                        readsize = 0;
                        Arrays.fill(audioData, (byte)0);
                    }
                    catch(Exception e) {
                        e.printStackTrace();
                    }
                }
            }
        };

        loop = true;
        workThread.start();
    }

    public void stopAudioIn() {
        loop = false;
        workThread.interrupt();
        mAudioRecord.stop();
        mAudioRecord.release();
        mAudioRecord = null;

        if (SAVE_FILE_FOR_TEST) {
            fileManager.closeFile();
            //測試代碼,以WAV格式保存數據啊
            PcmToWav.copyWaveFile(FileManager.TEST_PCM_FILE, FileManager.TEST_WAV_FILE, SAMPLE_HZ, bufferSizeInBytes);
        }
    }

我們再來看看MediaEncoder是如何編碼PCM裸音頻的

public MediaEncoder() {
        if (SAVE_FILE_FOR_TEST) {
            videoFileManager = new FileManager(FileManager.TEST_H264_FILE);
            audioFileManager = new FileManager(FileManager.TEST_AAC_FILE);
        }
        videoQueue = new LinkedBlockingQueue<>();
        audioQueue = new LinkedBlockingQueue<>();
        //這里我們初始化音頻數據,為什么要初始化音頻數據呢?音頻數據里面我們做了什么事情?
        audioEncodeBuffer = StreamProcessManager.encoderAudioInit(Contacts.SAMPLE_RATE,
                Contacts.CHANNELS, Contacts.BIT_RATE);
    }
............
public void startAudioEncode() {
        if (audioEncoderLoop) {
            throw new RuntimeException("必須先停止");
        }
        audioEncoderThread = new Thread() {
            @Override
            public void run() {
                byte[] outbuffer = new byte[1024];
                int haveCopyLength = 0;
                byte[] inbuffer = new byte[audioEncodeBuffer];
                while (audioEncoderLoop && !Thread.interrupted()) {
                    try {
                        AudioData audio = audioQueue.take();
                        //Log.e("RiemannLee", " audio.audioData.length " + audio.audioData.length + " audioEncodeBuffer " + audioEncodeBuffer);
                        final int audioGetLength = audio.audioData.length;
                        if (haveCopyLength < audioEncodeBuffer) {
                            System.arraycopy(audio.audioData, 0, inbuffer, haveCopyLength, audioGetLength);
                            haveCopyLength += audioGetLength;
                            int remain = audioEncodeBuffer - haveCopyLength;
                            if (remain == 0) {
                                int validLength = StreamProcessManager.encoderAudioEncode(inbuffer, audioEncodeBuffer, outbuffer, outbuffer.length);
                                //Log.e("lihuzi", " validLength " + validLength);
                                final int VALID_LENGTH = validLength;
                                if (VALID_LENGTH > 0) {
                                    byte[] encodeData = new byte[VALID_LENGTH];
                                    System.arraycopy(outbuffer, 0, encodeData, 0, VALID_LENGTH);
                                    if (sMediaEncoderCallback != null) {
                                        sMediaEncoderCallback.receiveEncoderAudioData(encodeData, VALID_LENGTH);
                                    }
                                    if (SAVE_FILE_FOR_TEST) {
                                        audioFileManager.saveFileData(encodeData);
                                    }
                                }
                                haveCopyLength = 0;
                            }
                        }
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                        break;
                    }
                }

            }
        };
        audioEncoderLoop = true;
        audioEncoderThread.start();
    }

 

進入audio的jni編碼

//音頻初始化
JNIEXPORT jint JNICALL Java_com_riemannlee_liveproject_StreamProcessManager_encoderAudioInit
        (JNIEnv *env, jclass type, jint jsampleRate, jint jchannels, jint jbitRate)
{
    audioEncoder = new AudioEncoder(jchannels, jsampleRate, jbitRate);
    int value = audioEncoder->init();
    return value;
}

現在,我們進入了AudioEncoder,進入了音頻編碼的世界

AudioEncoder::AudioEncoder(int channels, int sampleRate, int bitRate)
{
    this->channels = channels;
    this->sampleRate = sampleRate;
    this->bitRate = bitRate;
}
............
/**
 * 初始化fdk-aac的參數,設置相關接口使得
 * @return
 */
int AudioEncoder::init() {
    //打開AAC音頻編碼引擎,創建AAC編碼句柄
    if (aacEncOpen(&handle, 0, channels) != AACENC_OK) {
        LOGI("Unable to open fdkaac encodern");
        return -1;
    }
    // 下面都是利用aacEncoder_SetParam設置參數
    // AACENC_AOT設置為aac lc
    if (aacEncoder_SetParam(handle, AACENC_AOT, 2) != AACENC_OK) {
        LOGI("Unable to set the AOTn");
        return -1;
    }

    if (aacEncoder_SetParam(handle, AACENC_SAMPLERATE, sampleRate) != AACENC_OK) {
        LOGI("Unable to set the sampleRaten");
        return -1;
    }

    // AACENC_CHANNELMODE設置為雙通道
    if (aacEncoder_SetParam(handle, AACENC_CHANNELMODE, MODE_2) != AACENC_OK) {
        LOGI("Unable to set the channel moden");
        return -1;
    }

    if (aacEncoder_SetParam(handle, AACENC_CHANNELORDER, 1) != AACENC_OK) {
        LOGI("Unable to set the wav channel ordern");
        return 1;
    }
    if (aacEncoder_SetParam(handle, AACENC_BITRATE, bitRate) != AACENC_OK) {
        LOGI("Unable to set the bitraten");
        return -1;
    }
    if (aacEncoder_SetParam(handle, AACENC_TRANSMUX, 2) != AACENC_OK) { //0-raw 2-adts
        LOGI("Unable to set the ADTS transmuxn");
        return -1;
    }

    if (aacEncoder_SetParam(handle, AACENC_AFTERBURNER, 1) != AACENC_OK) {
        LOGI("Unable to set the ADTS AFTERBURNERn");
        return -1;
    }

    if (aacEncEncode(handle, NULL, NULL, NULL, NULL) != AACENC_OK) {
        LOGI("Unable to initialize the encodern");
        return -1;
    }

    AACENC_InfoStruct info = { 0 };
    if (aacEncInfo(handle, &info) != AACENC_OK) {
        LOGI("Unable to get the encoder infon");
        return -1;
    }

    //返回數據給上層,表示每次傳遞多少個數據最佳,這樣encode效率最高
    int inputSize = channels * 2 * info.frameLength;
    LOGI("inputSize = %d", inputSize);

    return inputSize;
}

我們終于知道MediaEncoder構造函數中初始化音頻數據的用意了,它會返回設備中傳遞多少inputSize為最佳,這樣,我們每次只需要傳遞相應的數據,就可以使得音頻效率更優化

public void startAudioEncode() {
        if (audioEncoderLoop) {
            throw new RuntimeException("必須先停止");
        }
        audioEncoderThread = new Thread() {
            @Override
            public void run() {
                byte[] outbuffer = new byte[1024];
                int haveCopyLength = 0;
                byte[] inbuffer = new byte[audioEncodeBuffer];
                while (audioEncoderLoop && !Thread.interrupted()) {
                    try {
                        AudioData audio = audioQueue.take();
                        //我們通過fdk-aac接口獲取到了audioEncodeBuffer的數據,即每次編碼多少數據為最優
                        //這里我這邊的手機每次都是返回的4096即4K的數據,其實為了簡單點,我們每次可以讓
                        //MIC錄取4K大小的數據,然后把錄取的數據傳遞到AudioEncoder.cpp中取編碼
                        //Log.e("RiemannLee", " audio.audioData.length " + audio.audioData.length + " audioEncodeBuffer " + audioEncodeBuffer);
                        final int audioGetLength = audio.audioData.length;
                        if (haveCopyLength < audioEncodeBuffer) {
                            System.arraycopy(audio.audioData, 0, inbuffer, haveCopyLength, audioGetLength);
                            haveCopyLength += audioGetLength;
                            int remain = audioEncodeBuffer - haveCopyLength;
                            if (remain == 0) {
                                //fdk-aac編碼PCM裸音頻數據,返回可用長度的有效字段
                                int validLength = StreamProcessManager.encoderAudioEncode(inbuffer, audioEncodeBuffer, outbuffer, outbuffer.length);
                                //Log.e("lihuzi", " validLength " + validLength);
                                final int VALID_LENGTH = validLength;
                                if (VALID_LENGTH > 0) {
                                    byte[] encodeData = new byte[VALID_LENGTH];
                                    System.arraycopy(outbuffer, 0, encodeData, 0, VALID_LENGTH);
                                    if (sMediaEncoderCallback != null) {
                                        //編碼后,把數據拋給rtmp去推流
                                        sMediaEncoderCallback.receiveEncoderAudioData(encodeData, VALID_LENGTH);
                                    }
                                    //我們可以把Fdk-aac編碼后的數據保存到文件中,然后用播放器聽一下,音頻文件是否編碼正確
                                    if (SAVE_FILE_FOR_TEST) {
                                        audioFileManager.saveFileData(encodeData);
                                    }
                                }
                                haveCopyLength = 0;
                            }
                        }
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                        break;
                    }
                }

            }
        };
        audioEncoderLoop = true;
        audioEncoderThread.start();
    }

我們看AudioEncoder是如何利用fdk-aac編碼的

/**
 * Fdk-AAC庫壓縮裸音頻PCM數據,轉化為AAC,這里為什么用fdk-aac,這個庫相比普通的aac庫,壓縮效率更高
 * @param inBytes
 * @param length
 * @param outBytes
 * @param outLength
 * @return
 */
int AudioEncoder::encodeAudio(unsigned char *inBytes, int length, unsigned char *outBytes, int outLength) {
    void *in_ptr, *out_ptr;
    AACENC_BufDesc in_buf = {0};
    int in_identifier = IN_AUDIO_DATA;
    int in_elem_size = 2;
    //傳遞input數據給in_buf
    in_ptr = inBytes;
    in_buf.bufs = &in_ptr;
    in_buf.numBufs = 1;
    in_buf.bufferIdentifiers = &in_identifier;
    in_buf.bufSizes = &length;
    in_buf.bufElSizes = &in_elem_size;

    AACENC_BufDesc out_buf = {0};
    int out_identifier = OUT_BITSTREAM_DATA;
    int elSize = 1;
    //out數據放到out_buf中
    out_ptr = outBytes;
    out_buf.bufs = &out_ptr;
    out_buf.numBufs = 1;
    out_buf.bufferIdentifiers = &out_identifier;
    out_buf.bufSizes = &outLength;
    out_buf.bufElSizes = &elSize;

    AACENC_InArgs in_args = {0};
    in_args.numInSamples = length / 2;  //size為pcm字節數

    AACENC_OutArgs out_args = {0};
    AACENC_ERROR err;

    //利用aacEncEncode來編碼PCM裸音頻數據,上面的代碼都是fdk-aac的流程步驟
    if ((err = aacEncEncode(handle, &in_buf, &out_buf, &in_args, &out_args)) != AACENC_OK) {
        LOGI("Encoding aac failedn");
        return err;
    }
    //返回編碼后的有效字段長度
    return out_args.numOutBytes;
}

至此,我們終于把視頻數據和音頻數據編碼成功了

視頻數據:NV21==>YUV420P==>H264
音頻數據:PCM裸音頻==>AAC

四 . RTMP如何推送音視頻流 最后我們看看rtmp是如何推流的:我們看看MediaPublisher這個類

    public MediaPublisher() {
        mediaEncoder = new MediaEncoder();

        MediaEncoder.setsMediaEncoderCallback(new MediaEncoder.MediaEncoderCallback() {
            @Override
            public void receiveEncoderVideoData(byte[] videoData, int totalLength, int[] segment) {
                onEncoderVideoData(videoData, totalLength, segment);
            }

            @Override
            public void receiveEncoderAudioData(byte[] audioData, int size) {
                onEncoderAudioData(audioData, size);
            }
        });

        rtmpThread = new Thread("publish-thread") {
            @Override
            public void run() {
                while (loop && !Thread.interrupted()) {
                    try {
                        Runnable runnable = mRunnables.take();
                        runnable.run();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        };

        loop = true;
        rtmpThread.start();
    }
............
    private void onEncoderVideoData(byte[] encodeVideoData, int totalLength, int[] segment) {
        int spsLen = 0;
        int ppsLen = 0;
        byte[] sps = null;
        byte[] pps = null;
        int haveCopy = 0;
        //segment為C++傳遞上來的數組,當為SPS,PPS的時候,視頻NALU數組大于1,其它時候等于1
        for (int i = 0; i < segment.length; i++) {
            int segmentLength = segment[i];
            byte[] segmentByte = new byte[segmentLength];
            System.arraycopy(encodeVideoData, haveCopy, segmentByte, 0, segmentLength);
            haveCopy += segmentLength;

            int offset = 4;
            if (segmentByte[2] == 0x01) {
                offset = 3;
            }
            int type = segmentByte[offset] & 0x1f;
            //Log.d("RiemannLee", "type= " + type);
            //獲取到NALU的type,SPS,PPS,SEI,還是關鍵幀
            if (type == NAL_SPS) {
                spsLen = segment[i] - 4;
                sps = new byte[spsLen];
                System.arraycopy(segmentByte, 4, sps, 0, spsLen);
                //Log.e("RiemannLee", "NAL_SPS spsLen " + spsLen);
            } else if (type == NAL_PPS) {
                ppsLen = segment[i] - 4;
                pps = new byte[ppsLen];
                System.arraycopy(segmentByte, 4, pps, 0, ppsLen);
                //Log.e("RiemannLee", "NAL_PPS ppsLen " + ppsLen);
                sendVideoSpsAndPPS(sps, spsLen, pps, ppsLen, 0);
            } else {
                sendVideoData(segmentByte, segmentLength, videoID++);
            }
        }
    }
............
   private void onEncoderAudioData(byte[] encodeAudioData, int size) {
        if (!isSendAudioSpec) {
            Log.e("RiemannLee", "#######sendAudioSpec######");
            sendAudioSpec(0);
            isSendAudioSpec = true;
        }
        sendAudioData(encodeAudioData, size, audioID++);
    }

向rtmp發送視頻和音頻數據的時候,實際上就是下面幾個JNI函數

   /**
     * 初始化RMTP,建立RTMP與RTMP服務器連接
     * @param url
     * @return
     */
    public static native int initRtmpData(String url);

    /**
     * 發送SPS,PPS數據
     * @param sps       sps數據
     * @param spsLen    sps長度
     * @param pps       pps數據
     * @param ppsLen    pps長度
     * @param timeStamp 時間戳
     * @return
     */
    public static native int sendRtmpVideoSpsPPS(byte[] sps, int spsLen, byte[] pps, int ppsLen, long timeStamp);

    /**
     * 發送視頻數據,再發送sps,pps之后
     * @param data
     * @param dataLen
     * @param timeStamp
     * @return
     */
    public static native int sendRtmpVideoData(byte[] data, int dataLen, long timeStamp);

    /**
     * 發送AAC Sequence HEAD 頭數據
     * @param timeStamp
     * @return
     */
    public static native int sendRtmpAudioSpec(long timeStamp);

    /**
     * 發送AAC音頻數據
     * @param data
     * @param dataLen
     * @param timeStamp
     * @return
     */
    public static native int sendRtmpAudioData(byte[] data, int dataLen, long timeStamp);

    /**
     * 釋放RTMP連接
     * @return
     */
    public static native int releaseRtmp();

再來看看RtmpLivePublish是如何完成這幾個jni函數的

//初始化rtmp,主要是在RtmpLivePublish類完成的
JNIEXPORT jint JNICALL Java_com_riemannlee_liveproject_StreamProcessManager_initRtmpData
        (JNIEnv *env, jclass type, jstring jurl)
{
    const char *url_cstr = env->GetStringUTFChars(jurl, NULL);
    //復制url_cstr內容到rtmp_path
    char *rtmp_path = (char*)malloc(strlen(url_cstr) + 1);
    memset(rtmp_path, 0, strlen(url_cstr) + 1);
    memcpy(rtmp_path, url_cstr, strlen(url_cstr));

    rtmpLivePublish = new RtmpLivePublish();
    rtmpLivePublish->init((unsigned char*)rtmp_path);

    return 0;
}

//發送sps,pps數據
JNIEXPORT jint JNICALL Java_com_riemannlee_liveproject_StreamProcessManager_sendRtmpVideoSpsPPS
        (JNIEnv *env, jclass type, jbyteArray jspsArray, jint spsLen, jbyteArray ppsArray, jint ppsLen, jlong jstamp)
{
    if (rtmpLivePublish) {
        jbyte *sps_data = env->GetByteArrayElements(jspsArray, NULL);
        jbyte *pps_data = env->GetByteArrayElements(ppsArray, NULL);

        rtmpLivePublish->addSequenceH264Header((unsigned char*) sps_data, spsLen, (unsigned char*) pps_data, ppsLen);

        env->ReleaseByteArrayElements(jspsArray, sps_data, 0);
        env->ReleaseByteArrayElements(ppsArray, pps_data, 0);
    }
    return 0;
}

//發送視頻數據
JNIEXPORT jint JNICALL Java_com_riemannlee_liveproject_StreamProcessManager_sendRtmpVideoData
        (JNIEnv *env, jclass type, jbyteArray jvideoData, jint dataLen, jlong jstamp)
{
    if (rtmpLivePublish) {
        jbyte *video_data = env->GetByteArrayElements(jvideoData, NULL);

        rtmpLivePublish->addH264Body((unsigned char*)video_data, dataLen, jstamp);

        env->ReleaseByteArrayElements(jvideoData, video_data, 0);
    }
    return 0;
}

//發送音頻Sequence頭數據
JNIEXPORT jint JNICALL Java_com_riemannlee_liveproject_StreamProcessManager_sendRtmpAudioSpec
        (JNIEnv *env, jclass type, jlong jstamp)
{
    if (rtmpLivePublish) {
        rtmpLivePublish->addSequenceAacHeader(44100, 2, 0);
    }
    return 0;
}

//發送音頻Audio數據
JNIEXPORT jint JNICALL Java_com_riemannlee_liveproject_StreamProcessManager_sendRtmpAudioData
        (JNIEnv *env, jclass type, jbyteArray jaudiodata, jint dataLen, jlong jstamp)
{
    if (rtmpLivePublish) {
        jbyte *audio_data = env->GetByteArrayElements(jaudiodata, NULL);

        rtmpLivePublish->addAccBody((unsigned char*) audio_data, dataLen, jstamp);

        env->ReleaseByteArrayElements(jaudiodata, audio_data, 0);
    }
    return 0;
}

//釋放RTMP連接
JNIEXPORT jint JNICALL Java_com_riemannlee_liveproject_StreamProcessManager_releaseRtmp
        (JNIEnv *env, jclass type)
{
    if (rtmpLivePublish) {
        rtmpLivePublish->release();
    }
    return 0;
}

最后再來看看RtmpLivePublish這個推流類是如何推送音視頻的,rtmp的音視頻流的推送有一個前提,需要首先發送

AVC sequence header 視頻同步包的構造
AAC sequence header 音頻同步包的構造

下面我們來看看AVC sequence的結構,AVC sequence header就是
AVCDecoderConfigurationRecord結構

 

這個協議對應于下面的代碼:

    /*AVCDecoderConfigurationRecord*/
    //configurationVersion版本號,1
    body[i++] = 0x01;
    //AVCProfileIndication sps[1]
    body[i++] = sps[1];
    //profile_compatibility sps[2]
    body[i++] = sps[2];
    //AVCLevelIndication sps[3]
    body[i++] = sps[3];
    //6bit的reserved為二進制位111111和2bitlengthSizeMinusOne一般為3,
    //二進制位11,合并起來為11111111,即為0xff
    body[i++] = 0xff;

    /*sps*/
    //3bit的reserved,二進制位111,5bit的numOfSequenceParameterSets,
    //sps個數,一般為1,及合起來二進制位11100001,即為0xe1
    body[i++]   = 0xe1;
    //SequenceParametersSetNALUnits(sps_size + sps)的數組
    body[i++] = (sps_len >> 8) & 0xff;
    body[i++] = sps_len & 0xff;
    memcpy(&body[i], sps, sps_len);
    i +=  sps_len;

    /*pps*/
    //numOfPictureParameterSets一般為1,即為0x01
    body[i++]   = 0x01;
    //SequenceParametersSetNALUnits(pps_size + pps)的數組
    body[i++] = (pps_len >> 8) & 0xff;
    body[i++] = (pps_len) & 0xff;
    memcpy(&body[i], pps, pps_len);
    i +=  pps_len;

對于AAC sequence header存放的是AudioSpecificConfig結構,該結構則在“ISO-14496-3 Audio”中描述。AudioSpecificConfig結構的描述非常復雜,這里我做一下簡化,事先設定要將要編碼的音頻格式,其中,選擇"AAC-LC"為音頻編碼,音頻采樣率為44100,于是AudioSpecificConfig簡化為下表:

 

這個協議對應于下面的代碼:

    //如上圖所示
    //5bit audioObjectType 編碼結構類型,AAC-LC為2 二進制位00010
    //4bit samplingFrequencyIndex 音頻采樣索引值,44100對應值是4,二進制位0100
    //4bit channelConfiguration 音頻輸出聲道,對應的值是2,二進制位0010
    //1bit frameLengthFlag 標志位用于表明IMDCT窗口長度 0 二進制位0
    //1bit dependsOnCoreCoder 標志位,表面是否依賴與corecoder 0 二進制位0
    //1bit extensionFlag 選擇了AAC-LC,這里必須是0 二進制位0
    //上面都合成二進制0001001000010000
    uint16_t audioConfig = 0 ;
    //這里的2表示對應的是AAC-LC 由于是5個bit,左移11位,變為16bit,2個字節
    //與上一個1111100000000000(0xF800),即只保留前5個bit
    audioConfig |= ((2 << 11) & 0xF800) ;

    int sampleRateIndex = getSampleRateIndex( sampleRate ) ;
    if( -1 == sampleRateIndex ) {
        free(packet);
        packet = NULL;
        LOGE("addSequenceAacHeader: no support current sampleRate[%d]" , sampleRate);
        return;
    }

    //sampleRateIndex為4,二進制位0000001000000000 & 0000011110000000(0x0780)(只保留5bit后4位)
    audioConfig |= ((sampleRateIndex << 7) & 0x0780) ;
    //sampleRateIndex為4,二進制位000000000000000 & 0000000001111000(0x78)(只保留5+4后4位)
    audioConfig |= ((channel << 3) & 0x78) ;
    //最后三個bit都為0保留最后三位111(0x07)
    audioConfig |= (0 & 0x07) ;
    //最后得到合成后的數據0001001000010000,然后分別取這兩個字節

    body[2] = ( audioConfig >> 8 ) & 0xFF ;
    body[3] = ( audioConfig & 0xFF );

至此,我們就分別構造了AVC sequence header 和AAC sequence header,這兩個結構是推流的先決條件,沒有這兩個東西,解碼器是無法解碼的,最后我們再來看看我們把解碼的音視頻如何rtmp推送

/**
 * 發送H264數據
 * @param buf
 * @param len
 * @param timeStamp
 */
void RtmpLivePublish::addH264Body(unsigned char *buf, int len, long timeStamp) {
    //去掉起始碼(界定符)
    if (buf[2] == 0x00) {
        //00 00 00 01
        buf += 4;
        len -= 4;
    } else if (buf[2] == 0x01) {
        // 00 00 01
        buf += 3;
        len -= 3;
    }
    int body_size = len + 9;
    RTMPPacket *packet = (RTMPPacket *)malloc(RTMP_HEAD_SIZE + 9 + len);
    memset(packet, 0, RTMP_HEAD_SIZE);
    packet->m_body = (char *)packet + RTMP_HEAD_SIZE;

    unsigned char *body = (unsigned char*)packet->m_body;
    //當NAL頭信息中,type(5位)等于5,說明這是關鍵幀NAL單元
    //buf[0] NAL Header與運算,獲取type,根據type判斷關鍵幀和普通幀
    //00000101 & 00011111(0x1f) = 00000101
    int type = buf[0] & 0x1f;
    //Pframe  7:AVC
    body[0] = 0x27;
    //IDR I幀圖像
    //Iframe  7:AVC
    if (type == NAL_SLICE_IDR) {
        body[0] = 0x17;
    }
    //AVCPacketType = 1
    /*nal unit,NALUs(AVCPacketType == 1)*/
    body[1] = 0x01;
    //composition time 0x000000 24bit
    body[2] = 0x00;
    body[3] = 0x00;
    body[4] = 0x00;

    //寫入NALU信息,右移8位,一個字節的讀取
    body[5] = (len >> 24) & 0xff;
    body[6] = (len >> 16) & 0xff;
    body[7] = (len >> 8) & 0xff;
    body[8] = (len) & 0xff;

    /*copy data*/
    memcpy(&body[9], buf, len);

    packet->m_hasAbsTimestamp = 0;
    packet->m_nBodySize = body_size;
    //當前packet的類型:Video
    packet->m_packetType = RTMP_PACKET_TYPE_VIDEO;
    packet->m_nChannel = 0x04;
    packet->m_headerType = RTMP_PACKET_SIZE_LARGE;
    packet->m_nInfoField2 = rtmp->m_stream_id;
    //記錄了每一個tag相對于第一個tag(File Header)的相對時間
    packet->m_nTimeStamp = RTMP_GetTime() - start_time;

    //send rtmp h264 body data
    if (RTMP_IsConnected(rtmp)) {
        RTMP_SendPacket(rtmp, packet, TRUE);
        //LOGD("send packet sendVideoData");
    }
    free(packet);
}

/**
 * 發送rtmp AAC data
 * @param buf
 * @param len
 * @param timeStamp
 */
void RtmpLivePublish::addAccBody(unsigned char *buf, int len, long timeStamp) {
    int body_size = 2 + len;
    RTMPPacket * packet = (RTMPPacket *)malloc(RTMP_HEAD_SIZE + len + 2);
    memset(packet, 0, RTMP_HEAD_SIZE);

    packet->m_body = (char *)packet + RTMP_HEAD_SIZE;
    unsigned char * body = (unsigned char *)packet->m_body;

    //頭信息配置
    /*AF 00 + AAC RAW data*/
    body[0] = 0xAF;
    //AACPacketType:1表示AAC raw
    body[1] = 0x01;
    /*spec_buf是AAC raw數據*/
    memcpy(&body[2], buf, len);
    packet->m_packetType = RTMP_PACKET_TYPE_AUDIO;
    packet->m_nBodySize = body_size;
    packet->m_nChannel = 0x04;
    packet->m_hasAbsTimestamp = 0;
    packet->m_headerType = RTMP_PACKET_SIZE_LARGE;
    packet->m_nTimeStamp = RTMP_GetTime() - start_time;
    //LOGI("aac m_nTimeStamp = %d", packet->m_nTimeStamp);
    packet->m_nInfoField2 = rtmp->m_stream_id;

    //send rtmp aac data
    if (RTMP_IsConnected(rtmp)) {
        RTMP_SendPacket(rtmp, packet, TRUE);
        //LOGD("send packet sendAccBody");
    }
    free(packet);
}

我們推送RTMP都是調用的libRtmp庫的RTMP_SendPacket接口,先判斷是否rtmp是通的,是的話推流即可,最后,我們看看rtmp是如何連接服務器的:

/**
 * 初始化RTMP數據,與rtmp連接
 * @param url
 */
void RtmpLivePublish::init(unsigned char * url) {
    this->rtmp_url = url;
    rtmp = RTMP_Alloc();
    RTMP_Init(rtmp);

    rtmp->Link.timeout = 5;
    RTMP_SetupURL(rtmp, (char *)url);
    RTMP_EnableWrite(rtmp);

    if (!RTMP_Connect(rtmp, NULL) ) {
        LOGI("RTMP_Connect error");
    } else {
        LOGI("RTMP_Connect success.");
    }

    if (!RTMP_ConnectStream(rtmp, 0)) {
        LOGI("RTMP_ConnectStream error");
    } else {
        LOGI("RTMP_ConnectStream success.");
    }
    start_time = RTMP_GetTime();
    LOGI(" start_time = %d", start_time);
}

至此,我們終于完成了rtmp推流的整個過程。

如果你對音視頻開發感興趣,覺得文章對您有幫助,別忘了點贊、收藏哦!或者對本文的一些闡述有自己的看法,有任何問題,歡迎在下方評論區與我討論!

分享到:
標簽:ffmpeg
用戶無頭像

網友整理

注冊時間:

網站:5 個   小程序:0 個  文章:12 篇

  • 51998

    網站

  • 12

    小程序

  • 1030137

    文章

  • 747

    會員

趕快注冊賬號,推廣您的網站吧!
最新入駐小程序

數獨大挑戰2018-06-03

數獨一種數學游戲,玩家需要根據9

答題星2018-06-03

您可以通過答題星輕松地創建試卷

全階人生考試2018-06-03

各種考試題,題庫,初中,高中,大學四六

運動步數有氧達人2018-06-03

記錄運動步數,積累氧氣值。還可偷

每日養生app2018-06-03

每日養生,天天健康

體育訓練成績評定2018-06-03

通用課目體育訓練成績評定