要理解RTMP推流,我們就要知道詳細原理,這方面的文章有很多,我也看到過學習過很多這樣的文章,但是很多都沒有詳細的去給大家展示,都沒有一個完整的流程,使得初學者難以弄懂其中的原理,下面我將詳細的來給大家介紹RTMP推流原理以及如何推送到伺服器,首先我們瞭解一下推流的全過程:

本工程的原始碼地址如下: RiemannLeeLiveProject
我們將會分為幾個小節來展開:
一. 本文用到的庫檔案:
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格式:


所以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();
}
複製程式碼
這個就是如何把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 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 < 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 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推流的整個過程。
五 . 程式碼以及如何除錯
本工程的原始碼地址如下: RiemannLeeLiveProject
如何搭建RTMP伺服器,可以參考下面網址 https://www.cnblogs.com/jys509/p/5649066.html 然後再執行命令:nginx, 然後再伺服器端開啟VLC播放器,輸入伺服器IP地址,推流過來雙擊即可使用VLC播放器展示我們的推流結果:

最後附上兩張效果圖


其實,也可以不用這些庫檔案來進行轉碼,現在android裝置一般都會支援h264和aac的轉碼,MediaCodec就支援,而且是java端程式碼,它是谷歌支援的硬編碼,上手快,而且使用比較簡單,效率也高。由於本人水平有限,裡面如果有錯誤的地方,望大家包涵,一起學習,謝謝!
同步釋出於:Android中使用ffmpeg編碼進行rtmp推流
參考文件:
http://www.cnblogs.com/jingzhishen/p/3965868.html
http://www.cnblogs.com/lidabo/p/4602422.html
http://blog.csdn.net/chenchong_219/article/details/37990541
http://blog.csdn.net/mincheat/article/details/48713047
http://guoh.org/lifelog/2013/10/h-264-bit-stream-sps-pps-idr-nalu/
http://lazybing.github.io/blog/2017/06/22/sodb-rbsp-ebsp/
https://www.jianshu.com/p/ea3fadae9d47
http://www.cnblogs.com/lihaiping/p/4167844.html
http://blog.csdn.net/a992036795/article/details/54572335
http://blog.csdn.net/u011003120/article/details/78378632