新四季網

ffmpeg支持rtsp推流嗎(Android中使用ffmpeg編碼進行rtmp推流)

2023-04-13 20:45:53 2

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

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

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

一. 本文用到的庫文件:

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

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

LOCAL_PATH := $(call my-dir)include $(CLEAR_VARS)LOCAL_MODULE := avformatLOCAL_SRC_FileS := $(LOCAL_PATH)/lib/libavformat.ainclude $(PREBUILT_static_LIBRARY)include $(CLEAR_VARS)LOCAL_MODULE := avcodecLOCAL_SRC_FILES := $(LOCAL_PATH)/lib/libavcodec.ainclude $(PREBUILT_STATIC_LIBRARY)include $(CLEAR_VARS)LOCAL_MODULE := swscaleLOCAL_SRC_FILES := $(LOCAL_PATH)/lib/libswscale.ainclude $(PREBUILT_STATIC_LIBRARY)include $(CLEAR_VARS)LOCAL_MODULE := avutilLOCAL_SRC_FILES := $(LOCAL_PATH)/lib/libavutil.ainclude $(PREBUILT_STATIC_LIBRARY)include $(CLEAR_VARS)LOCAL_MODULE := swresampleLOCAL_SRC_FILES := $(LOCAL_PATH)/lib/libswresample.ainclude $(PREBUILT_STATIC_LIBRARY)include $(CLEAR_VARS)LOCAL_MODULE := postprocLOCAL_SRC_FILES := $(LOCAL_PATH)/lib/libpostproc.ainclude $(PREBUILT_STATIC_LIBRARY)include $(CLEAR_VARS)LOCAL_MODULE := x264LOCAL_SRC_FILES := $(LOCAL_PATH)/lib/libx264.ainclude $(PREBUILT_STATIC_LIBRARY)include $(CLEAR_VARS)LOCAL_MODULE := libyuvLOCAL_SRC_FILES := $(LOCAL_PATH)/lib/libyuv.ainclude $(PREBUILT_STATIC_LIBRARY)include $(CLEAR_VARS)LOCAL_MODULE := libfdk-aac#LOCAL_C_INCLUDES = $(LOCAL_PATH)/include/LOCAL_SRC_FILES := $(LOCAL_PATH)/lib/libfdk-aac.ainclude $(PREBUILT_STATIC_LIBRARY)include $(CLEAR_VARS)LOCAL_MODULE := polarsslLOCAL_SRC_FILES := $(LOCAL_PATH)/lib/libpolarssl.ainclude $(PREBUILT_STATIC_LIBRARY)include $(CLEAR_VARS)LOCAL_MODULE := rtmpLOCAL_SRC_FILES := $(LOCAL_PATH)/lib/librtmp.ainclude $(PREBUILT_STATIC_LIBRARY)include $(CLEAR_VARS)LOCAL_MODULE := libstreamLOCAL_SRC_FILES := StreamProcess.cpp FrameEncoder.cpp AudioEncoder.cpp wavreader.c RtmpLivePublish.cppLOCAL_C_INCLUDES = $(LOCAL_PATH)/include/LOCAL_STATIC_LIBRARIES := libyuv avformat avcodec swscale avutil swresample postproc x264 libfdk-aac polarssl rtmpLOCAL_LDLIBS = -L$(LOCAL_PATH)/prebuilt/ -llog -lz -Ipthreadinclude $(BUILD_SHARED_LIBRARY)

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

【相關學習資料推薦,點擊下方連結免費報名,報名後會彈出學習資料免費領取地址~】

【免費】FFmpeg/WebRTC/RTMP/NDK/Android音視頻流媒體高級開發-學習視頻教程-騰訊課堂

C 音視頻更多學習資料:點擊莬費領取→音視頻開發(資料文檔 視頻教程 面試題)(FFmpeg WebRTC RTMP RTSP HLS RTP)

二 . 如何從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 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個字節

【相關學習資料推薦,點擊下方連結免費報名,報名後會彈出學習資料免費領取地址~】

【免費】FFmpeg/WebRTC/RTMP/NDK/Android音視頻流媒體高級開發-學習視頻教程-騰訊課堂

下面我們來看看兩個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 videoQueue; //音頻流隊列 private LinkedBlockingQueue 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 videoQueue; //音頻流隊列 private LinkedBlockingQueue 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 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層是對核心算法引擎,塊,宏塊及片的語法級別的定義,他最終輸出編碼完的數據 SODBSODB:String of Data Bits,數據比特串,它是最原始的編碼數據RBSP:Raw Byte Sequence Payload,原始字節序載荷,它是在SODB的後面添加了結尾比特和若干比特0,以便字節對齊EBSP:Encapsulate Byte Sequence Payload,擴展字節序列載荷,它是在RBSP基礎上添加了防校驗字節0x03後得到的。關係大致如下:SODB RBSP STOP bit 0bits = RBSPRBSP part1 0x03 RBSP part2 0x03 … RBSP partn = EBSPNALU 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單元。

【相關學習資料推薦,點擊下方連結免費報名,報名後會彈出學習資料免費領取地址~】

【免費】FFmpeg/WebRTC/RTMP/NDK/Android音視頻流媒體高級開發-學習視頻教程-騰訊課堂

C 音視頻更多學習資料:點擊莬費領取→音視頻開發(資料文檔 視頻教程 面試題)(FFmpeg WebRTC RTMP RTSP HLS RTP)

對應的代碼定義如下

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信息,後面即為我們說的EBSP00 00 00 01 68 ... 這個為PPS00 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; // 是否採用長前綴碼0x00000001int 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_2FrameEncoder::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 > 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 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; }

【相關學習資料推薦,點擊下方連結免費報名,報名後會彈出學習資料免費領取地址~】

【免費】FFmpeg/WebRTC/RTMP/NDK/Android音視頻流媒體高級開發-學習視頻教程-騰訊課堂

C 音視頻更多學習資料:點擊莬費領取→音視頻開發(資料文檔 視頻教程 面試題)(FFmpeg WebRTC RTMP RTSP HLS RTP)

進入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 encoder\n"); return -1; } // 下面都是利用aacEncoder_SetParam設置參數 // AACENC_AOT設置為aac lc if (aacEncoder_SetParam(handle, AACENC_AOT, 2) != AACENC_OK) { LOGI("Unable to set the AOT\n"); return -1; } if (aacEncoder_SetParam(handle, AACENC_SAMPLERATE, sampleRate) != AACENC_OK) { LOGI("Unable to set the sampleRate\n"); return -1; } // AACENC_CHANNELMODE設置為雙通道 if (aacEncoder_SetParam(handle, AACENC_CHANNELMODE, MODE_2) != AACENC_OK) { LOGI("Unable to set the channel mode\n"); return -1; } if (aacEncoder_SetParam(handle, AACENC_CHANNELORDER, 1) != AACENC_OK) { LOGI("Unable to set the wav channel order\n"); return 1; } if (aacEncoder_SetParam(handle, AACENC_BITRATE, bitRate) != AACENC_OK) { LOGI("Unable to set the bitrate\n"); return -1; } if (aacEncoder_SetParam(handle, AACENC_TRANSMUX, 2) != AACENC_OK) { //0-raw 2-adts LOGI("Unable to set the ADTS transmux\n"); return -1; } if (aacEncoder_SetParam(handle, AACENC_AFTERBURNER, 1) != AACENC_OK) { LOGI("Unable to set the ADTS AFTERBURNER\n"); return -1; } if (aacEncEncode(handle, NULL, NULL, NULL, NULL) != AACENC_OK) { LOGI("Unable to initialize the encoder\n"); return -1; } AACENC_InfoStruct info = { 0 }; if (aacEncInfo(handle, &info) != AACENC_OK) { LOGI("Unable to get the encoder info\n"); 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 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 = 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 = out_buf.bufElSizes = 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 failed\n"); 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推流的整個過程。

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

,
同类文章
葬禮的夢想

葬禮的夢想

夢見葬禮,我得到了這個夢想,五個要素的五個要素,水火只好,主要名字在外面,職業生涯良好,一切都應該對待他人治療誠意,由於小,吉利的冬天夢想,秋天的夢是不吉利的
找到手機是什麼意思?

找到手機是什麼意思?

找到手機是什麼意思?五次選舉的五個要素是兩名士兵的跡象。與他溝通很好。這是非常財富,它擅長運作,職業是仙人的標誌。單身男人有這個夢想,主要生活可以有人幫忙
我不怎麼想?

我不怎麼想?

我做了什麼意味著看到米飯烹飪?我得到了這個夢想,五線的主要土壤,但是Tu Ke水是錢的跡象,職業生涯更加真誠。他真誠地誠實。這是豐富的,這是夏瑞的巨星
夢想你的意思是什麼?

夢想你的意思是什麼?

你是什​​麼意思夢想的夢想?夢想,主要木材的五個要素,水的跡象,主營業務,主營業務,案子應該抓住魅力,不能疏忽,春天夢想的吉利夢想夏天的夢想不幸。詢問學者夢想
拯救夢想

拯救夢想

拯救夢想什麼意思?你夢想著拯救人嗎?拯救人們的夢想有一個現實,也有夢想的主觀想像力,請參閱週宮官方網站拯救人民夢想的詳細解釋。夢想著敵人被拯救出來
2022愛方向和生日是在[質量個性]中

2022愛方向和生日是在[質量個性]中

[救生員]有人說,在出生88天之前,胎兒已經知道哪天的出生,如何有優質的個性,將走在什麼樣的愛情之旅,將與生活生活有什么生活。今天
夢想切割剪裁

夢想切割剪裁

夢想切割剪裁什麼意思?你夢想切你的手是好的嗎?夢想切割手工切割手有一個真正的影響和反應,也有夢想的主觀想像力。請參閱官方網站夢想的細節,以削減手
夢想著親人死了

夢想著親人死了

夢想著親人死了什麼意思?你夢想夢想你的親人死嗎?夢想有一個現實的影響和反應,還有夢想的主觀想像力,請參閱夢想世界夢想死亡的親屬的詳細解釋
夢想搶劫

夢想搶劫

夢想搶劫什麼意思?你夢想搶劫嗎?夢想著搶劫有一個現實的影響和反應,也有夢想的主觀想像力,請參閱週恭吉夢官方網站的詳細解釋。夢想搶劫
夢想缺乏缺乏紊亂

夢想缺乏缺乏紊亂

夢想缺乏缺乏紊亂什麼意思?你夢想缺乏異常藥物嗎?夢想缺乏現實世界的影響和現實,還有夢想的主觀想像,請看官方網站的夢想組織缺乏異常藥物。我覺得有些東西缺失了