要理解RTMP推流,我們就要知道詳細(xì)原理。
本文將詳細(xì)的來(lái)給大家介紹RTMP推流原理以及如何推送到服務(wù)器,首先我們了解一下推流的全過(guò)程:
我們將會(huì)分為幾個(gè)小節(jié)來(lái)展開(kāi):
一. 本文用到的庫(kù)文件:
1.1 本項(xiàng)目用到的庫(kù)文件如下圖所示,用到了ffmpeg庫(kù),以及編碼視頻的x264,編碼音頻的fdk-aac,推流使用的rtmp等:
使用靜態(tài)鏈接庫(kù),最終把這些.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)
具體使用到哪些庫(kù)中的接口我們將再下面進(jìn)行細(xì)節(jié)展示。
二 . 如何從Camera攝像頭獲取視頻流:
2.1 Camera獲取視頻流,這個(gè)就不用多說(shuō)了,只需要看到這個(gè)回調(diào)就行了,我們需要獲取到這個(gè)數(shù)據(jù):
//CameraSurfaceView.JAVA中
@Override
public void onPreviewFrame(byte[] data, Camera camera) {
camera.addCallbackBuffer(data);
if (listener != null) {
listener.onCallback(data);
}
}
//阻塞線程安全隊(duì)列,生產(chǎn)者和消費(fèi)者
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轉(zhuǎn)化為YUV420P數(shù)據(jù) 我們知道一般的攝像頭的數(shù)據(jù)都是NV21或者是NV12,接下來(lái)我們會(huì)用到第一個(gè)編碼庫(kù)libyuv庫(kù),我們先來(lái)看看這個(gè)消費(fèi)者怎么從NV21的數(shù)據(jù)轉(zhuǎn)化為YUV的
workThread = new Thread() {
@Override
public void run() {
while (loop && !Thread.interrupted()) {
try {
//獲取阻塞隊(duì)列中的數(shù)據(jù),沒(méi)有數(shù)據(jù)的時(shí)候阻塞
byte[] srcData = mQueue.take();
//生成I420(YUV標(biāo)準(zhǔn)格式數(shù)據(jù)及YUV420P)目標(biāo)數(shù)據(jù),
//生成后的數(shù)據(jù)長(zhǎng)度width * height * 3 / 2
final byte[] dstData = new byte[scaleWidth * scaleHeight * 3 / 2];
final int morientation = mCameraUtil.getMorientation();
//壓縮NV21(YUV420SP)數(shù)據(jù),元素?cái)?shù)據(jù)位1080 * 1920,很顯然
//這樣的數(shù)據(jù)推流會(huì)很占用帶寬,我們壓縮成480 * 640 的YUV數(shù)據(jù)
//為啥要轉(zhuǎn)化為YUV420P數(shù)據(jù)?因?yàn)槭窃跒檗D(zhuǎn)化為H264數(shù)據(jù)在做
//準(zhǔn)備,NV21不是標(biāo)準(zhǔn)的,只能先通過(guò)轉(zhuǎn)換,生成標(biāo)準(zhǔn)YUV420P數(shù)據(jù),
//然后把標(biāo)準(zhǔn)數(shù)據(jù)encode為H264流
StreamProcessManager.compressYUV(srcData, mCameraUtil.getCameraWidth(), mCameraUtil.getCameraHeight(), dstData, scaleHeight, scaleWidth, 0, morientation, morientation == 270);
//進(jìn)行YUV420P數(shù)據(jù)裁剪的操作,測(cè)試下這個(gè)借口,
//我們可以對(duì)數(shù)據(jù)進(jìn)行裁剪,裁剪后的數(shù)據(jù)也是I420數(shù)據(jù),
//我們采用的是libyuv庫(kù)文件
//這個(gè)libyuv庫(kù)效率非常高,這也是我們用它的原因
final byte[] cropData = new byte[cropWidth * cropHeight * 3 / 2];
StreamProcessManager.cropYUV(dstData, scaleWidth, scaleHeight, cropData, cropWidth, cropHeight, cropStartX, cropStartY);
//自此,我們得到了YUV420P標(biāo)準(zhǔn)數(shù)據(jù),這個(gè)過(guò)程實(shí)際上就是NV21轉(zhuǎn)化為YUV420P數(shù)據(jù)
//注意,有些機(jī)器是NV12格式,只是數(shù)據(jù)存儲(chǔ)不一樣,我們一樣可以用libyuv庫(kù)的接口轉(zhuǎn)化
if (yuvDataListener != null) {
yuvDataListener.onYUVDataReceiver(cropData, cropWidth, cropHeight);
}
//設(shè)置為true,我們把生成的YUV文件用播放器播放一下,看我們
//的數(shù)據(jù)是否有誤,起調(diào)試作用
if (SAVE_FILE_FOR_TEST) {
fileManager.saveFileData(cropData);
}
} catch (InterruptedException e) {
e.printStackTrace();
break;
}
}
}
};
2.3 介紹一下攝像頭的數(shù)據(jù)流格式
視頻流的轉(zhuǎn)換,android中一般攝像頭的格式是NV21或者是NV12,它們都是YUV420sp的一種,那么什么是YUV格式呢?
何為YUV格式,有三個(gè)分量,Y表示明亮度,也就是灰度值,U和V則表示色度,即影像色彩飽和度,用于指定像素的顏色,(直接點(diǎn)就是Y是亮度信息,UV是色彩信息),YUV格式分為兩大類(lèi),planar和packed兩種:
對(duì)于planar的YUV格式,先連續(xù)存儲(chǔ)所有像素點(diǎn)Y,緊接著存儲(chǔ)所有像素點(diǎn)U,隨后所有像素點(diǎn)V
對(duì)于packed的YUV格式,每個(gè)像素點(diǎn)YUV是連續(xù)交替存儲(chǔ)的
YUV格式為什么后面還帶數(shù)字呢,比如YUV 420,444,442 YUV444:每一個(gè)Y對(duì)應(yīng)一組UV分量 YUV422:每?jī)蓚€(gè)Y共用一組UV分量 YUV420:每四個(gè)Y公用一組UV分量
實(shí)際上NV21,NV12就是屬于YUV420,是一種two-plane模式,即Y和UV分為兩個(gè)Plane,UV為交錯(cuò)存儲(chǔ),他們都屬于YUV420SP,舉個(gè)例子就會(huì)很清晰了
NV21格式數(shù)據(jù)排列方式是YYYYYYYY(w*h)VUVUVUVU(w*h/2),
對(duì)于NV12的格式,排列方式是YYYYYYYY(w*h)UVUVUVUV(w*h/2)
正如代碼注釋中所說(shuō)的那樣,我們以標(biāo)準(zhǔn)的YUV420P為例,對(duì)于這樣的格式,我們要取出Y,U,V這三個(gè)分量,我們看怎么取?
比如480 * 640大小的圖片,其字節(jié)數(shù)為 480 * 640 * 3 >> 1個(gè)字節(jié)
Y分量:480 * 640個(gè)字節(jié)
U分量:480 * 640 >>2個(gè)字節(jié)
V分量:480 * 640 >>2個(gè)字節(jié),加起來(lái)就為480 * 640 * 3 >> 1個(gè)字節(jié)
存儲(chǔ)都是行優(yōu)先存儲(chǔ),三部分之間順序是YUV依次存儲(chǔ),即
0 ~ 480*640是Y分量;480 * 640 ~ 480 * 640 * 5 / 4為U分量;480 * 640 * 5 / 4 ~ 480 * 640 * 3 / 2是V分量,
記住這個(gè)計(jì)算方法,等下在JNI中馬上會(huì)體現(xiàn)出來(lái)
那么YUV420SP和YUV420P的區(qū)別在哪里呢?顯然Y的排序是完全相同的,但是UV排列上原理是完全不同的,420P它是先吧U存放完后,再放V,也就是說(shuō)UV是連續(xù)的,而420SP它是UV,UV這樣交替存放: YUV420SP格式:
YUV420P格式:
所以NV21(YUV420SP)的數(shù)據(jù)如下: 同樣的以480 * 640大小的圖片為例,其字節(jié)數(shù)為 480 * 640 * 3 >> 1個(gè)字節(jié) Y分量:480 * 640個(gè)字節(jié) UV分量:480 * 640 >>1個(gè)字節(jié)(注意,我們沒(méi)有把UV分量分開(kāi)) 加起來(lái)就為480 * 640 * 3 >> 1個(gè)字節(jié)
下面我們來(lái)看看兩個(gè)JNI函數(shù),這個(gè)是攝像頭轉(zhuǎn)化的兩個(gè)最關(guān)鍵的函數(shù)
/**
* NV21轉(zhuǎn)化為YUV420P數(shù)據(jù)
* @param src 原始數(shù)據(jù)
* @param width 原始數(shù)據(jù)寬度
* @param height 原始數(shù)據(jù)高度
* @param dst 生成數(shù)據(jù)
* @param dst_width 生成數(shù)據(jù)寬度
* @param dst_height 生成數(shù)據(jù)高度
* @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數(shù)據(jù)的裁剪
* @param src 原始數(shù)據(jù)
* @param width 原始數(shù)據(jù)寬度
* @param height 原始數(shù)據(jù)高度
* @param dst 生成數(shù)據(jù)
* @param dst_width 生成數(shù)據(jù)寬度
* @param dst_height 生成數(shù)據(jù)高度
* @param left 裁剪的起始x點(diǎn)
* @param top 裁剪的起始y點(diǎn)
* @return
*/
public static native int cropYUV(byte[] src, int width, int height, byte[] dst, int dst_width, int dst_height, int left, int top);
再看一看具體實(shí)現(xiàn)
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轉(zhuǎn)化為i420(標(biāo)準(zhǔn)YUV420P數(shù)據(jù)) 這個(gè)temp_i420_data大小是和Src_data是一樣的
nv21ToI420(Src_data, width, height, temp_i420_data);
//進(jìn)行縮放的操作,這個(gè)縮放,會(huì)把數(shù)據(jù)壓縮
scaleI420(temp_i420_data, width, height, temp_i420_data_scale, dst_width, dst_height, mode);
//如果是前置攝像頭,進(jìn)行鏡像操作
if (isMirror) {
//進(jìn)行旋轉(zhuǎn)的操作
rotateI420(temp_i420_data_scale, dst_width, dst_height, temp_i420_data_rotate, degree);
//因?yàn)樾D(zhuǎn)的角度都是90和270,那后面的數(shù)據(jù)width和height是相反的
mirrorI420(temp_i420_data_rotate, dst_height, dst_width, Dst_data);
} else {
//進(jìn)行旋轉(zhuǎn)的操作
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層傳遞過(guò)來(lái)的參數(shù)可以看到,原始數(shù)據(jù)是1080 * 1920,先轉(zhuǎn)為1080 * 1920的標(biāo)準(zhǔn)的YUV420P的數(shù)據(jù),下面的代碼就是上面我舉的例子,如何拆分YUV420P的Y,U,V分量和如何拆分YUV420SP的Y,UV分量,最后調(diào)用libyuv庫(kù)的libyuv::NV21ToI420數(shù)據(jù)就完成了轉(zhuǎn)換;然后進(jìn)行縮放,調(diào)用了libyuv::I420Scale的函數(shù)完成轉(zhuǎn)換
//NV21轉(zhuǎn)化為YUV420P數(shù)據(jù)
void nv21ToI420(jbyte *src_nv21_data, jint width, jint height, jbyte *src_i420_data) {
//Y通道數(shù)據(jù)大小
jint src_y_size = width * height;
//U通道數(shù)據(jù)大小
jint src_u_size = (width >> 1) * (height >> 1);
//NV21中Y通道數(shù)據(jù)
jbyte *src_nv21_y_data = src_nv21_data;
//由于是連續(xù)存儲(chǔ)的Y通道數(shù)據(jù)后即為VU數(shù)據(jù),它們的存儲(chǔ)方式是交叉存儲(chǔ)的
jbyte *src_nv21_vu_data = src_nv21_data + src_y_size;
//YUV420P中Y通道數(shù)據(jù)
jbyte *src_i420_y_data = src_i420_data;
//YUV420P中U通道數(shù)據(jù)
jbyte *src_i420_u_data = src_i420_data + src_y_size;
//YUV420P中V通道數(shù)據(jù)
jbyte *src_i420_v_data = src_i420_data + src_y_size + src_u_size;
//直接調(diào)用libyuv中接口,把NV21數(shù)據(jù)轉(zhuǎn)化為YUV420P標(biāo)準(zhǔn)數(shù)據(jù),此時(shí),它們的存儲(chǔ)大小是不變的
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);
}
//進(jìn)行縮放操作,此時(shí)是把1080 * 1920的YUV420P的數(shù)據(jù) ==> 480 * 640的YUV420P的數(shù)據(jù)
void scaleI420(jbyte *src_i420_data, jint width, jint height, jbyte *dst_i420_data, jint dst_width,
jint dst_height, jint mode) {
//Y數(shù)據(jù)大小width*height,U數(shù)據(jù)大小為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);
//由于是標(biāo)準(zhǔn)的YUV420P的數(shù)據(jù),我們可以把三個(gè)通道全部分離出來(lái)
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;
//由于是標(biāo)準(zhǔn)的YUV420P的數(shù)據(jù),我們可以把三個(gè)通道全部分離出來(lái)
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;
//調(diào)用libyuv庫(kù),進(jìn)行縮放操作
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數(shù)據(jù)轉(zhuǎn)化為YUV420P的標(biāo)準(zhǔn)數(shù)據(jù)了,這樣,我們就可以把這個(gè)數(shù)據(jù)流轉(zhuǎn)化為H264了,接下來(lái),我們來(lái)看看如何把YUV420P流數(shù)據(jù)轉(zhuǎn)化為h264數(shù)據(jù),從而為推流做準(zhǔn)備
三 標(biāo)準(zhǔn)YUV420P數(shù)據(jù)編碼為H264
多說(shuō)無(wú)用,直接上代碼
3.1 代碼如何實(shí)現(xiàn)h264編碼的:
/**
* 編碼類(lèi)MediaEncoder,主要是把視頻流YUV420P格式編碼為h264格式,把PCM裸音頻轉(zhuǎn)化為AAC格式
*/
public class MediaEncoder {
private static final String TAG = "MediaEncoder";
private Thread videoEncoderThread, audioEncoderThread;
private boolean videoEncoderLoop, audioEncoderLoop;
//視頻流隊(duì)列
private LinkedBlockingQueue<VideoData> videoQueue;
//音頻流隊(duì)列
private LinkedBlockingQueue<AudioData> audioQueue;
.........
//攝像頭的YUV420P數(shù)據(jù),put到隊(duì)列中,生產(chǎn)者模型
public void putVideoData(VideoData videoData) {
try {
videoQueue.put(videoData);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
.........
videoEncoderThread = new Thread() {
@Override
public void run() {
//視頻消費(fèi)者模型,不斷從隊(duì)列中取出視頻流來(lái)進(jìn)行h264編碼
while (videoEncoderLoop && !Thread.interrupted()) {
try {
//隊(duì)列中取視頻數(shù)據(jù)
VideoData videoData = videoQueue.take();
fps++;
byte[] outbuffer = new byte[videoData.width * videoData.height];
int[] buffLength = new int[10];
//對(duì)YUV420P進(jìn)行h264編碼,返回一個(gè)數(shù)據(jù)大小,里面是編碼出來(lái)的h264數(shù)據(jù)
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數(shù)據(jù)
byte[] encodeData = new byte[totalLength];
System.arraycopy(outbuffer, 0, encodeData, 0, encodeData.length);
if (sMediaEncoderCallback != null) {
sMediaEncoderCallback.receiveEncoderVideoData(encodeData, encodeData.length, segment);
}
//我們可以把數(shù)據(jù)在java層保存到文件中,看看我們編碼的h264數(shù)據(jù)是否能播放,h264裸數(shù)據(jù)可以在VLC播放器中播放
if (SAVE_FILE_FOR_TEST) {
videoFileManager.saveFileData(encodeData);
}
}
} catch (InterruptedException e) {
e.printStackTrace();
break;
}
}
}
};
videoEncoderLoop = true;
videoEncoderThread.start();
}
至此,我們就把攝像頭的NV21數(shù)據(jù)轉(zhuǎn)化為YUV420P的標(biāo)準(zhǔn)數(shù)據(jù)了,這樣,我們就可以把這個(gè)數(shù)據(jù)流轉(zhuǎn)化為H264了,接下來(lái),我們來(lái)看看如何把YUV420P流數(shù)據(jù)轉(zhuǎn)化為h264數(shù)據(jù),從而為推流做準(zhǔn)備
三 標(biāo)準(zhǔn)YUV420P數(shù)據(jù)編碼為H264
多說(shuō)無(wú)用,直接上代碼
3.1 代碼如何實(shí)現(xiàn)h264編碼的:
/**
* 編碼類(lèi)MediaEncoder,主要是把視頻流YUV420P格式編碼為h264格式,把PCM裸音頻轉(zhuǎn)化為AAC格式
*/
public class MediaEncoder {
private static final String TAG = "MediaEncoder";
private Thread videoEncoderThread, audioEncoderThread;
private boolean videoEncoderLoop, audioEncoderLoop;
//視頻流隊(duì)列
private LinkedBlockingQueue<VideoData> videoQueue;
//音頻流隊(duì)列
private LinkedBlockingQueue<AudioData> audioQueue;
.........
//攝像頭的YUV420P數(shù)據(jù),put到隊(duì)列中,生產(chǎn)者模型
public void putVideoData(VideoData videoData) {
try {
videoQueue.put(videoData);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
.........
videoEncoderThread = new Thread() {
@Override
public void run() {
//視頻消費(fèi)者模型,不斷從隊(duì)列中取出視頻流來(lái)進(jìn)行h264編碼
while (videoEncoderLoop && !Thread.interrupted()) {
try {
//隊(duì)列中取視頻數(shù)據(jù)
VideoData videoData = videoQueue.take();
fps++;
byte[] outbuffer = new byte[videoData.width * videoData.height];
int[] buffLength = new int[10];
//對(duì)YUV420P進(jìn)行h264編碼,返回一個(gè)數(shù)據(jù)大小,里面是編碼出來(lái)的h264數(shù)據(jù)
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數(shù)據(jù)
byte[] encodeData = new byte[totalLength];
System.arraycopy(outbuffer, 0, encodeData, 0, encodeData.length);
if (sMediaEncoderCallback != null) {
sMediaEncoderCallback.receiveEncoderVideoData(encodeData, encodeData.length, segment);
}
//我們可以把數(shù)據(jù)在java層保存到文件中,看看我們編碼的h264數(shù)據(jù)是否能播放,h264裸數(shù)據(jù)可以在VLC播放器中播放
if (SAVE_FILE_FOR_TEST) {
videoFileManager.saveFileData(encodeData);
}
}
} catch (InterruptedException e) {
e.printStackTrace();
break;
}
}
}
};
videoEncoderLoop = true;
videoEncoderThread.start();
}
這個(gè)就是如何把YUV420P數(shù)據(jù)轉(zhuǎn)化為h264流,主要代碼是這個(gè)JNI函數(shù),接下來(lái)我們看是如何編碼成h264的,編碼函數(shù)如下:
/**
* 編碼視頻數(shù)據(jù)接口
* @param srcFrame 原始數(shù)據(jù)(YUV420P數(shù)據(jù))
* @param frameSize 幀大小
* @param fps fps
* @param dstFrame 編碼后的數(shù)據(jù)存儲(chǔ)
* @param outFramewSize 編碼后的數(shù)據(jù)大小
* @return
*/
public static native int encoderVideoEncode(byte[] srcFrame, int frameSize, int fps, byte[] dstFrame, int[] outFramewSize);
JNI中視頻流的編碼接口,我們看到的是初始化一個(gè)FrameEncoder類(lèi),然后調(diào)用這個(gè)類(lèi)的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;
}
//視頻編碼主要函數(shù),注意JNI函數(shù)GetByteArrayElements和ReleaseByteArrayElements成對(duì)出現(xiàn),否則回內(nèi)存泄露
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;
}
下面我們來(lái)詳細(xì)的分析FrameEncoder這個(gè)C++類(lèi),這里我們用到了多個(gè)庫(kù),第一個(gè)就是鼎鼎大名的ffmpeg庫(kù),還有就是X264庫(kù),下面我們先來(lái)了解一下h264的文件結(jié)構(gòu),這樣有利于我們理解h264的編碼流程
3.2 h264我們必須知道的一些概念:
首先我們來(lái)介紹h264字節(jié)流,先來(lái)了解下面幾個(gè)概念,h264由哪些東西組成呢?
1.VCL video coding layer 視頻編碼層;
2.NAL .NETwork abstraction layer 網(wǎng)絡(luò)提取層;
其中,VCL層是對(duì)核心算法引擎,塊,宏塊及片的語(yǔ)法級(jí)別的定義,他最終輸出編碼完的數(shù)據(jù) SODB
SODB:String of Data Bits,數(shù)據(jù)比特串,它是最原始的編碼數(shù)據(jù)
RBSP:Raw Byte Sequence Payload,原始字節(jié)序載荷,它是在SODB的后面添加了結(jié)尾比特和若干比特0,以便字節(jié)對(duì)齊
EBSP:Encapsulate Byte Sequence Payload,擴(kuò)展字節(jié)序列載荷,它是在RBSP基礎(chǔ)上添加了防校驗(yàn)字節(jié)0x03后得到的。
關(guān)系大致如下:
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頭結(jié)構(gòu)
長(zhǎng)度:1byte(1個(gè)字節(jié))
forbidden_bit(1bit) + nal_reference_bit(2bit) + nal_unit_type(5bit)
1. forbidden_bit:禁止位,初始為0,當(dāng)網(wǎng)絡(luò)發(fā)現(xiàn)NAL單元有比特錯(cuò)誤時(shí)可設(shè)置該比特為1,以便接收方糾錯(cuò)或
丟掉該單元。
2. nal_reference_bit:nal重要性指示,標(biāo)志該NAL單元的重要性,值越大,越重要,解碼器在解碼處理不過(guò)來(lái)
的時(shí)候,可以丟掉重要性為0的NALU。
NALU類(lèi)型結(jié)構(gòu)圖:
其中,nal_unit_type為1, 2, 3, 4, 5及12的NAL單元稱(chēng)為VCL的NAL單元,其他類(lèi)型的NAL單元為非VCL的NAL單元。
對(duì)應(yīng)的代碼定義如下
public static final int NAL_UNKNOWN = 0;
public static final int NAL_SLICE = 1; /* 非關(guān)鍵幀 */
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; /* 關(guān)鍵幀 */
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字節(jié)流,就是由一些start code + NALU組成的,要組成一個(gè)NALU單元,首先要有原始數(shù)據(jù),稱(chēng)之為SODB,它是原始的H264數(shù)據(jù)編碼得到到,不包括3字節(jié)(0x000001)/4字節(jié)(0x00000001)的start code,也不會(huì)包括1字節(jié)的NALU頭, NALU頭部信息包括了一些基礎(chǔ)信息,比如NALU類(lèi)型。 ps:起始碼包括兩種,3字節(jié)0x000001和4字節(jié)0x00000001,在sps和pps和Access Unit的第一個(gè)NALU使用4字節(jié)起始碼,其余情況均使用3字節(jié)起始碼
在 H264 SPEC 中,RBSP 定義如下: 在SODB結(jié)束處添加表示結(jié)束的bit 1來(lái)表示SODB已經(jīng)結(jié)束,因此添加的bit 1成為rbsp_stop_one_bit,RBSP也需要字節(jié)對(duì)齊,為此需要在rbsp_stop_one_bit后添加若干0補(bǔ)齊,簡(jiǎn)單來(lái)說(shuō),要在SODB后面追加兩樣?xùn)|西就形成了RBSP rbsp_stop_one_bit = 1 rbsp_alignment_zero_bit(s) = 0(s)
RBSP的生成過(guò)程:
即RBSP最后一個(gè)字節(jié)包含SODB最后幾個(gè)比特,以及trailing bits其中,第一個(gè)比特位1,其余的比特位0,保證字節(jié)對(duì)齊,最后再結(jié)尾處添加0x0000,即CABAC_ZERO_word,從而形成 RBSP。
EBSP的生成過(guò)程:NALU數(shù)據(jù)+起始碼就形成了AnnexB格式(下面有介紹H264的兩種格式,AnnexB為常用的格式),起始碼包括兩種,0x000001和0x00000001,為了不讓NALU的主體和起始碼之間產(chǎn)生競(jìng)爭(zhēng),在對(duì)RBSP進(jìn)行掃描的時(shí)候,如果遇到連續(xù)兩個(gè)0x00字節(jié),則在該兩個(gè)字節(jié)后面添加一個(gè)0x03字節(jié),在解碼的時(shí)候?qū)⒃?x03字節(jié)去掉,也稱(chēng)為脫殼操作。解碼器在解碼時(shí),首先逐個(gè)字節(jié)讀取NAL的數(shù)據(jù),統(tǒng)計(jì)NAL的長(zhǎng)度,然后再開(kāi)始解碼。 替換規(guī)則如下: 0x000000 => 0x00000300 0x000001 => 0x00000301 0x000002 => 0x00000302 0x000003 => 0x00000303
3.3 下面我們找一個(gè)h264文件來(lái)看看
00 00 00 01 67 ... 這個(gè)為SPS,67為NALU Header,有type信息,后面即為我們說(shuō)的EBSP
00 00 00 01 68 ... 這個(gè)為PPS
00 00 01 06 ... 為SEI補(bǔ)充增強(qiáng)信息
00 00 01 65... 為IDR關(guān)鍵幀,圖像中的編碼slice
對(duì)于這個(gè)SPS集合,從67type后開(kāi)始計(jì)算, 即42 c0 33 a6 80 b4 1e 68 40 00 00 03 00 40 00 00 0c a3 c6 0c a8 正如前面的描述,解碼的時(shí)候直接03 這個(gè)03是競(jìng)爭(zhēng)檢測(cè)
從前面我們分析知道,VCL層出來(lái)的是編碼完的視頻幀數(shù)據(jù),這些幀可能是I,B,P幀,而且這些幀可能屬于不同的序列,在這同一個(gè)序列還有相對(duì)應(yīng)的一套序列參數(shù)集和圖片參數(shù)集,所以要完成視頻的解碼,不僅需要傳輸VCL層編碼出來(lái)的視頻幀數(shù)據(jù),還需要傳輸序列參數(shù)集,圖像參數(shù)集等數(shù)據(jù)。
參數(shù)集:包括序列參數(shù)集SPS和圖像參數(shù)集PPS
SPS:包含的是針對(duì)一連續(xù)編碼視頻序列的參數(shù),如標(biāo)識(shí)符seq_parameter_set_id,幀數(shù)以及POC的約束,參數(shù)幀數(shù)目,解碼圖像尺寸和幀場(chǎng)編碼模式選擇標(biāo)識(shí)等等 PPS:對(duì)應(yīng)的是一個(gè)序列中某一副圖像或者某幾幅圖像,其參數(shù)如標(biāo)識(shí)符pic_parameter_set_id、可選的 seq_parameter_set_id、熵編碼模式選擇標(biāo)識(shí),片組數(shù)目,初始量化參數(shù)和去方塊濾波系數(shù)調(diào)整標(biāo)識(shí)等等 數(shù)據(jù)分割:組成片的編碼數(shù)據(jù)存放在3個(gè)獨(dú)立的DP(數(shù)據(jù)分割A(yù),B,C)中,各自包含一個(gè)編碼片的子集, 分割A(yù)包含片頭和片中宏塊頭數(shù)據(jù) 分割B包含幀內(nèi)和 SI 片宏塊的編碼殘差數(shù)據(jù)。 分割 C包含幀間宏塊的編碼殘差數(shù)據(jù)。 每個(gè)分割可放在獨(dú)立的 NAL 單元并獨(dú)立傳輸。
NALU的順序要求 H264/AVC標(biāo)準(zhǔn)對(duì)送到解碼器的NAL單元是由嚴(yán)格要求的,如果NAL單元的順序是混亂的,必須將其重新依照規(guī)范組織后送入解碼器,否則不能正確解碼
1. 序列參數(shù)集NAL單元
必須在傳送所有以此參數(shù)集為參考的其它NAL單元之前傳送,不過(guò)允許這些NAL單元中中間出現(xiàn)重復(fù)的序列參數(shù)集合NAL單元。
所謂重復(fù)的詳細(xì)解釋為:序列參數(shù)集NAL單元都有其專(zhuān)門(mén)的標(biāo)識(shí),如果兩個(gè)序列參數(shù)集NAL單元的標(biāo)識(shí)相同,就可以認(rèn)為后一個(gè)只不過(guò)是前一個(gè)的拷貝,而非新的序列參數(shù)集
2. 圖像參數(shù)集NAL單元
必須在所有此參數(shù)集為參考的其它NAL單元之前傳送,不過(guò)允許這些NAL單元中間出現(xiàn)重復(fù)的圖像參數(shù)集NAL單元,這一點(diǎn)與上述的序列參數(shù)集NAL單元是相同的。
3. 不同基本編碼圖像中的片段(slice)單元和數(shù)據(jù)劃分片段(data partition)單元在順序上不可以相互交叉,即不允許屬于某一基本編碼圖像的一系列片段(slice)單元和數(shù)據(jù)劃分片段(data partition)單元中忽然出現(xiàn)另一個(gè)基本編碼圖像的片段(slice)單元片段和數(shù)據(jù)劃分片段(data partition)單元。
4. 參考圖像的影響:如果一幅圖像以另一幅圖像為參考,則屬于前者的所有片段(slice)單元和數(shù)據(jù)劃分片段(data partition)單元必須在屬于后者的片段和數(shù)據(jù)劃分片段之后,無(wú)論是基本編碼圖像還是冗余編碼圖像都必須遵守這個(gè)規(guī)則。
5. 基本編碼圖像的所有片段(slice)單元和數(shù)據(jù)劃分片段(data partition)單元必須在屬于相應(yīng)冗余編碼圖像的片段(slice)單元和數(shù)據(jù)劃分片段(data partition)單元之前。
6. 如果數(shù)據(jù)流中出現(xiàn)了連續(xù)的無(wú)參考基本編碼圖像,則圖像序號(hào)小的在前面。
7. 如果arbitrary_slice_order_allowed_flag置為1,一個(gè)基本編碼圖像中的片段(slice)單元和數(shù)據(jù)劃分片段(data partition)單元的順序是任意的,如果arbitrary_slice_order_allowed_flag置為零,則要按照片段中第一個(gè)宏塊的位置來(lái)確定片段的順序,若使用數(shù)據(jù)劃分,則A類(lèi)數(shù)據(jù)劃分片段在B類(lèi)數(shù)據(jù)劃分片段之前,B類(lèi)數(shù)據(jù)劃分片段在C類(lèi)數(shù)據(jù)劃分片段之前,而且對(duì)應(yīng)不同片段的數(shù)據(jù)劃分片段不能相互交叉,也不能與沒(méi)有數(shù)據(jù)劃分的片段相互交叉。
8. 如果存在SEI(補(bǔ)充增強(qiáng)信息)單元的話,它必須在它所對(duì)應(yīng)的基本編碼圖像的片段(slice)單元和數(shù)據(jù)劃分片段(data partition)單元之前,并同時(shí)必須緊接在上一個(gè)基本編碼圖像的所有片段(slice)單元和數(shù)據(jù)劃分片段(data partition)單元后邊。假如SEI屬于多個(gè)基本編碼圖像,其順序僅以第一個(gè)基本編碼圖像為參照。
9. 如果存在圖像分割符的話,它必須在所有SEI 單元、基本編碼圖像的所有片段slice)單元和數(shù)據(jù)劃分片段(data partition)單元之前,并且緊接著上一個(gè)基本編碼圖像那些NAL單元。
10. 如果存在序列結(jié)束符,且序列結(jié)束符后還有圖像,則該圖像必須是IDR(即時(shí)解碼器刷新)圖像。序列結(jié)束符的位置應(yīng)當(dāng)在屬于這個(gè)IDR圖像的分割符、SEI 單元等數(shù)據(jù)之前,且緊接著前面那些圖像的NAL單元。如果序列結(jié)束符后沒(méi)有圖像了,那么它的就在比特流中所有圖像數(shù)據(jù)之后。
11. 流結(jié)束符在比特流中的最后。
h264有兩種封裝, 一種是Annexb模式,傳統(tǒng)模式,有startcode,SPS和PPS是在ES中 一種是mp4模式,一般mp4 mkv會(huì)有,沒(méi)有startcode,SPS和PPS以及其它信息被封裝在container中,每一個(gè)frame前面是這個(gè)frame的長(zhǎng)度 很多解碼器只支持annexb這種模式,因此需要將mp4做轉(zhuǎn)換 我們討論的是第一種Annexb傳統(tǒng)模式,
3.4 下面我們直接看代碼,了解一下如何使用X264來(lái)編碼h264文件
x264_param_default_preset():為了方便使用x264,只需要根據(jù)編碼速度的要求和視頻質(zhì)量的要求選擇模型,
并修改部分視頻參數(shù)即可
x264_picture_alloc():為圖像結(jié)構(gòu)體x264_picture_t分配內(nèi)存。
x264_encoder_open():打開(kāi)編碼器。
x264_encoder_encode():編碼一幀圖像。
x264_encoder_close():關(guān)閉編碼器。
x264_picture_clean():釋放x264_picture_alloc()申請(qǐng)的資源。
存儲(chǔ)數(shù)據(jù)的結(jié)構(gòu)體如下所示。
x264_picture_t:存儲(chǔ)壓縮編碼前的像素?cái)?shù)據(jù)。
x264_nal_t:存儲(chǔ)壓縮編碼后的碼流數(shù)據(jù)。
下面介紹幾個(gè)重要的結(jié)構(gòu)體
/********************************************************************************************
x264_image_t 結(jié)構(gòu)用于存放一幀圖像實(shí)際像素?cái)?shù)據(jù)。該結(jié)構(gòu)體定義在x264.h中
*********************************************************************************************/
typedef struct
{
int i_csp; // 設(shè)置彩色空間,通常取值 X264_CSP_I420,所有可能取值定義在x264.h中
int i_plane; // 圖像平面?zhèn)€數(shù),例如彩色空間是YUV420格式的,此處取值3
int i_stride[4]; // 每個(gè)圖像平面的跨度,也就是每一行數(shù)據(jù)的字節(jié)數(shù)
uint8_t *plane[4]; // 每個(gè)圖像平面存放數(shù)據(jù)的起始地址, plane[0]是Y平面,
// plane[1]和plane[2]分別代表U和V平面
} x264_image_t;
/********************************************************************************************
x264_picture_t 結(jié)構(gòu)體描述視頻幀的特征,該結(jié)構(gòu)體定義在x264.h中。
*********************************************************************************************/
typedef struct
{
int i_type; // 幀的類(lèi)型,取值有X264_TYPE_KEYFRAME X264_TYPE_P
// X264_TYPE_AUTO等。初始化為auto,則在編碼過(guò)程自行控制。
int i_qpplus1; // 此參數(shù)減1代表當(dāng)前幀的量化參數(shù)值
int i_pic_struct; // 幀的結(jié)構(gòu)類(lèi)型,表示是幀還是場(chǎng),是逐行還是隔行,
// 取值為枚舉值 pic_struct_e,定義在x264.h中
int b_keyframe; // 輸出:是否是關(guān)鍵幀
int64_t i_pts; // 一幀的顯示時(shí)間戳
int64_t i_dts; // 輸出:解碼時(shí)間戳。當(dāng)一幀的pts非常接近0時(shí),該dts值可能為負(fù)。
/* 編碼器參數(shù)設(shè)置,如果為NULL則表示繼續(xù)使用前一幀的設(shè)置。某些參數(shù)
(例如aspect ratio) 由于收到H264本身的限制,只能每隔一個(gè)GOP才能改變。
這種情況下,如果想讓這些改變的參數(shù)立即生效,則必須強(qiáng)制生成一個(gè)IDR幀。*/
x264_param_t *param;
x264_image_t img; // 存放一幀圖像的真實(shí)數(shù)據(jù)
x264_image_properties_t prop;
x264_hrd_t hrd_timing;// 輸出:HRD時(shí)間信息,僅當(dāng)i_nal_hrd設(shè)置了才有效
void *opaque; // 私有數(shù)據(jù)存放區(qū),將輸入數(shù)據(jù)拷貝到輸出幀中
} x264_picture_t ;
/****************************************************************************************************************
x264_nal_t中的數(shù)據(jù)在下一次調(diào)用x264_encoder_encode之后就無(wú)效了,因此必須在調(diào)用
x264_encoder_encode 或 x264_encoder_headers 之前使用或拷貝其中的數(shù)據(jù)。
*****************************************************************************************************************/
typedef struct
{
int i_ref_idc; // Nal的優(yōu)先級(jí)
int i_type; // Nal的類(lèi)型
int b_long_startcode; // 是否采用長(zhǎng)前綴碼0x00000001
int i_first_mb; // 如果Nal為一條帶,則表示該條帶第一個(gè)宏塊的指數(shù)
int i_last_mb; // 如果Nal為一條帶,則表示該條帶最后一個(gè)宏塊的指數(shù)
int i_payload; // payload 的字節(jié)大小
uint8_t *p_payload; // 存放編碼后的數(shù)據(jù),已經(jīng)封裝成Nal單元
} x264_nal_t;
再來(lái)看看編碼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 源文件
//供測(cè)試文件使用,測(cè)試的時(shí)候打開(kāi)
//#define ENCODE_OUT_FILE_1
//供測(cè)試文件使用
//#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();
//按照色度空間分配內(nèi)存,即為圖像結(jié)構(gòu)體x264_picture_t分配內(nèi)存,并返回內(nèi)存的首地址作為指針
//i_csp(圖像顏色空間參數(shù),目前只支持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 打開(kāi)編碼器
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數(shù)據(jù)轉(zhuǎn)化為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數(shù)據(jù)保存到pic_in.img的對(duì)應(yīng)的分量中,還有一種方法是用AV_fillPicture和sws_scale來(lái)進(jìn)行變換
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;
//最主要的函數(shù),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數(shù)據(jù)保存為nal了,我們可以獲取到nals[i].type的類(lèi)型判斷是sps還是pps
//或者是否是關(guān)鍵幀,nals[i].i_payload表示數(shù)據(jù)長(zhǎng)度,nals[i].p_payload表示存儲(chǔ)的數(shù)據(jù)
//編碼后,我們按照nals[i].i_payload的長(zhǎng)度來(lái)保存copy h264數(shù)據(jù)的,然后拋給java端用作
//rtmp發(fā)送數(shù)據(jù),outFrameSize是變長(zhǎng)的,當(dāng)有sps pps的時(shí)候大于1,其它時(shí)候值為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;
}
最后,我們來(lái)看看拋往java層的h264數(shù)據(jù),在MediaEncoder.java中,函數(shù)startVideoEncode:
public void startVideoEncode() {
if (videoEncoderLoop) {
throw new RuntimeException("必須先停止");
}
videoEncoderThread = new Thread() {
@Override
public void run() {
//視頻消費(fèi)者模型,不斷從隊(duì)列中取出視頻流來(lái)進(jìn)行h264編碼
while (videoEncoderLoop && !Thread.interrupted()) {
try {
//隊(duì)列中取視頻數(shù)據(jù)
VideoData videoData = videoQueue.take();
fps++;
byte[] outbuffer = new byte[videoData.width * videoData.height];
int[] buffLength = new int[10];
//對(duì)YUV420P進(jìn)行h264編碼,返回一個(gè)數(shù)據(jù)大小,里面是編碼出來(lái)的h264數(shù)據(jù)
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數(shù)據(jù)
byte[] encodeData = new byte[totalLength];
System.arraycopy(outbuffer, 0, encodeData, 0, encodeData.length);
if (sMediaEncoderCallback != null) {
sMediaEncoderCallback.receiveEncoderVideoData(encodeData, encodeData.length, segment);
}
//我們可以把數(shù)據(jù)在java層保存到文件中,看看我們編碼的h264數(shù)據(jù)是否能播放,h264裸數(shù)據(jù)可以在VLC播放器中播放
if (SAVE_FILE_FOR_TEST) {
videoFileManager.saveFileData(encodeData);
}
}
} catch (InterruptedException e) {
e.printStackTrace();
break;
}
}
}
};
videoEncoderLoop = true;
videoEncoderThread.start();
}
此時(shí),h264數(shù)據(jù)已經(jīng)出來(lái)了,我們就實(shí)現(xiàn)了YUV420P的數(shù)據(jù)到H264數(shù)據(jù)的編碼,接下來(lái),我們?cè)賮?lái)看看音頻數(shù)據(jù)。
3.5 android音頻數(shù)據(jù)如何使用fdk-aac庫(kù)來(lái)編碼音頻,轉(zhuǎn)化為AAC數(shù)據(jù)的,直接上代碼
public class AudioRecoderManager {
private static final String TAG = "AudioRecoderManager";
// 音頻獲取
private final static int SOURCE = MediaRecorder.AudIOSource.MIC;
// 設(shè)置音頻采樣率,44100是目前的標(biāo)準(zhǔn),但是某些設(shè)備仍然支 2050 6000 1025
private final static int SAMPLE_HZ = 44100;
// 設(shè)置音頻的錄制的聲道CHANNEL_IN_STEREO為雙聲道,CHANNEL_CONFIGURATION_MONO為單聲道
private final static int CHANNEL_CONFIG = AudioFormat.CHANNEL_IN_STEREO;
// 音頻數(shù)據(jù)格式:PCM 16位每個(gè)樣本保證設(shè)備支持。PCM 8位每個(gè)樣本 不一定能得到設(shè)備支持
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裸音頻,這個(gè)音頻數(shù)據(jù)文件很大,我們必須編碼成AAC,這樣才能rtmp傳輸
while (loop && !Thread.interrupted()) {
try {
readsize += mAudioRecord.read(audioData, readsize, mBufferSize);
byte[] ralAudio = new byte[readsize];
//每次錄音讀取4K數(shù)據(jù)
System.arraycopy(audioData, 0, ralAudio, 0, readsize);
if (audioDataListener != null) {
//把錄音的數(shù)據(jù)拋給MediaEncoder去編碼AAC音頻數(shù)據(jù)
audioDataListener.audioData(ralAudio);
}
//我們可以把裸音頻以文件格式存起來(lái),判斷這個(gè)音頻是否是好的,只需要加一個(gè)WAV頭
//即形成WAV無(wú)損音頻格式
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();
//測(cè)試代碼,以WAV格式保存數(shù)據(jù)啊
PcmToWav.copyWaveFile(FileManager.TEST_PCM_FILE, FileManager.TEST_WAV_FILE, SAMPLE_HZ, bufferSizeInBytes);
}
}
我們?cè)賮?lái)看看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<>();
//這里我們初始化音頻數(shù)據(jù),為什么要初始化音頻數(shù)據(jù)呢?音頻數(shù)據(jù)里面我們做了什么事情?
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();
}
進(jìn)入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;
}
現(xiàn)在,我們進(jìn)入了AudioEncoder,進(jìn)入了音頻編碼的世界
AudioEncoder::AudioEncoder(int channels, int sampleRate, int bitRate)
{
this->channels = channels;
this->sampleRate = sampleRate;
this->bitRate = bitRate;
}
............
/**
* 初始化fdk-aac的參數(shù),設(shè)置相關(guān)接口使得
* @return
*/
int AudioEncoder::init() {
//打開(kāi)AAC音頻編碼引擎,創(chuàng)建AAC編碼句柄
if (aacEncOpen(&handle, 0, channels) != AACENC_OK) {
LOGI("Unable to open fdkaac encodern");
return -1;
}
// 下面都是利用aacEncoder_SetParam設(shè)置參數(shù)
// AACENC_AOT設(shè)置為aac lc
if (aacEncoder_SetParam(handle, AACENC_AOT, 2) != AACENC_OK) {
LOGI("Unable to set the AOTn");
return -1;
}
if (aacEncoder_SetParam(handle, AACENC_SAMPLERATE, sampleRate) != AACENC_OK) {
LOGI("Unable to set the sampleRaten");
return -1;
}
// AACENC_CHANNELMODE設(shè)置為雙通道
if (aacEncoder_SetParam(handle, AACENC_CHANNELMODE, MODE_2) != AACENC_OK) {
LOGI("Unable to set the channel moden");
return -1;
}
if (aacEncoder_SetParam(handle, AACENC_CHANNELORDER, 1) != AACENC_OK) {
LOGI("Unable to set the wav channel ordern");
return 1;
}
if (aacEncoder_SetParam(handle, AACENC_BITRATE, bitRate) != AACENC_OK) {
LOGI("Unable to set the bitraten");
return -1;
}
if (aacEncoder_SetParam(handle, AACENC_TRANSMUX, 2) != AACENC_OK) { //0-raw 2-adts
LOGI("Unable to set the ADTS transmuxn");
return -1;
}
if (aacEncoder_SetParam(handle, AACENC_AFTERBURNER, 1) != AACENC_OK) {
LOGI("Unable to set the ADTS AFTERBURNERn");
return -1;
}
if (aacEncEncode(handle, NULL, NULL, NULL, NULL) != AACENC_OK) {
LOGI("Unable to initialize the encodern");
return -1;
}
AACENC_InfoStruct info = { 0 };
if (aacEncInfo(handle, &info) != AACENC_OK) {
LOGI("Unable to get the encoder infon");
return -1;
}
//返回?cái)?shù)據(jù)給上層,表示每次傳遞多少個(gè)數(shù)據(jù)最佳,這樣encode效率最高
int inputSize = channels * 2 * info.frameLength;
LOGI("inputSize = %d", inputSize);
return inputSize;
}
我們終于知道MediaEncoder構(gòu)造函數(shù)中初始化音頻數(shù)據(jù)的用意了,它會(huì)返回設(shè)備中傳遞多少inputSize為最佳,這樣,我們每次只需要傳遞相應(yīng)的數(shù)據(jù),就可以使得音頻效率更優(yōu)化
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();
//我們通過(guò)fdk-aac接口獲取到了audioEncodeBuffer的數(shù)據(jù),即每次編碼多少數(shù)據(jù)為最優(yōu)
//這里我這邊的手機(jī)每次都是返回的4096即4K的數(shù)據(jù),其實(shí)為了簡(jiǎn)單點(diǎn),我們每次可以讓
//MIC錄取4K大小的數(shù)據(jù),然后把錄取的數(shù)據(jù)傳遞到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裸音頻數(shù)據(jù),返回可用長(zhǎng)度的有效字段
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) {
//編碼后,把數(shù)據(jù)拋給rtmp去推流
sMediaEncoderCallback.receiveEncoderAudioData(encodeData, VALID_LENGTH);
}
//我們可以把Fdk-aac編碼后的數(shù)據(jù)保存到文件中,然后用播放器聽(tīng)一下,音頻文件是否編碼正確
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庫(kù)壓縮裸音頻PCM數(shù)據(jù),轉(zhuǎn)化為AAC,這里為什么用fdk-aac,這個(gè)庫(kù)相比普通的aac庫(kù),壓縮效率更高
* @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數(shù)據(jù)給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數(shù)據(jù)放到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字節(jié)數(shù)
AACENC_OutArgs out_args = {0};
AACENC_ERROR err;
//利用aacEncEncode來(lái)編碼PCM裸音頻數(shù)據(jù),上面的代碼都是fdk-aac的流程步驟
if ((err = aacEncEncode(handle, &in_buf, &out_buf, &in_args, &out_args)) != AACENC_OK) {
LOGI("Encoding aac failedn");
return err;
}
//返回編碼后的有效字段長(zhǎng)度
return out_args.numOutBytes;
}
至此,我們終于把視頻數(shù)據(jù)和音頻數(shù)據(jù)編碼成功了
視頻數(shù)據(jù):NV21==>YUV420P==>H264
音頻數(shù)據(jù):PCM裸音頻==>AAC
四 . RTMP如何推送音視頻流 最后我們看看rtmp是如何推流的:我們看看MediaPublisher這個(gè)類(lèi)
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++傳遞上來(lái)的數(shù)組,當(dāng)為SPS,PPS的時(shí)候,視頻NALU數(shù)組大于1,其它時(shí)候等于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,還是關(guān)鍵幀
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發(fā)送視頻和音頻數(shù)據(jù)的時(shí)候,實(shí)際上就是下面幾個(gè)JNI函數(shù)
/**
* 初始化RMTP,建立RTMP與RTMP服務(wù)器連接
* @param url
* @return
*/
public static native int initRtmpData(String url);
/**
* 發(fā)送SPS,PPS數(shù)據(jù)
* @param sps sps數(shù)據(jù)
* @param spsLen sps長(zhǎng)度
* @param pps pps數(shù)據(jù)
* @param ppsLen pps長(zhǎng)度
* @param timeStamp 時(shí)間戳
* @return
*/
public static native int sendRtmpVideoSpsPPS(byte[] sps, int spsLen, byte[] pps, int ppsLen, long timeStamp);
/**
* 發(fā)送視頻數(shù)據(jù),再發(fā)送sps,pps之后
* @param data
* @param dataLen
* @param timeStamp
* @return
*/
public static native int sendRtmpVideoData(byte[] data, int dataLen, long timeStamp);
/**
* 發(fā)送AAC Sequence HEAD 頭數(shù)據(jù)
* @param timeStamp
* @return
*/
public static native int sendRtmpAudioSpec(long timeStamp);
/**
* 發(fā)送AAC音頻數(shù)據(jù)
* @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();
再來(lái)看看RtmpLivePublish是如何完成這幾個(gè)jni函數(shù)的
//初始化rtmp,主要是在RtmpLivePublish類(lèi)完成的
JNIEXPORT jint JNICALL Java_com_riemannlee_liveproject_StreamProcessManager_initRtmpData
(JNIEnv *env, jclass type, jstring jurl)
{
const char *url_cstr = env->GetStringUTFChars(jurl, NULL);
//復(fù)制url_cstr內(nèi)容到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;
}
//發(fā)送sps,pps數(shù)據(jù)
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;
}
//發(fā)送視頻數(shù)據(jù)
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;
}
//發(fā)送音頻Sequence頭數(shù)據(jù)
JNIEXPORT jint JNICALL Java_com_riemannlee_liveproject_StreamProcessManager_sendRtmpAudioSpec
(JNIEnv *env, jclass type, jlong jstamp)
{
if (rtmpLivePublish) {
rtmpLivePublish->addSequenceAacHeader(44100, 2, 0);
}
return 0;
}
//發(fā)送音頻Audio數(shù)據(jù)
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;
}
最后再來(lái)看看RtmpLivePublish這個(gè)推流類(lèi)是如何推送音視頻的,rtmp的音視頻流的推送有一個(gè)前提,需要首先發(fā)送
AVC sequence header 視頻同步包的構(gòu)造
AAC sequence header 音頻同步包的構(gòu)造
下面我們來(lái)看看AVC sequence的結(jié)構(gòu),AVC sequence header就是
AVCDecoderConfigurationRecord結(jié)構(gòu)
這個(gè)協(xié)議對(duì)應(yīng)于下面的代碼:
/*AVCDecoderConfigurationRecord*/
//configurationVersion版本號(hào),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為二進(jìn)制位111111和2bitlengthSizeMinusOne一般為3,
//二進(jìn)制位11,合并起來(lái)為11111111,即為0xff
body[i++] = 0xff;
/*sps*/
//3bit的reserved,二進(jìn)制位111,5bit的numOfSequenceParameterSets,
//sps個(gè)數(shù),一般為1,及合起來(lái)二進(jìn)制位11100001,即為0xe1
body[i++] = 0xe1;
//SequenceParametersSetNALUnits(sps_size + sps)的數(shù)組
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)的數(shù)組
body[i++] = (pps_len >> 8) & 0xff;
body[i++] = (pps_len) & 0xff;
memcpy(&body[i], pps, pps_len);
i += pps_len;
對(duì)于AAC sequence header存放的是AudioSpecificConfig結(jié)構(gòu),該結(jié)構(gòu)則在“ISO-14496-3 Audio”中描述。AudioSpecificConfig結(jié)構(gòu)的描述非常復(fù)雜,這里我做一下簡(jiǎn)化,事先設(shè)定要將要編碼的音頻格式,其中,選擇"AAC-LC"為音頻編碼,音頻采樣率為44100,于是AudioSpecificConfig簡(jiǎn)化為下表:
這個(gè)協(xié)議對(duì)應(yīng)于下面的代碼:
//如上圖所示
//5bit audioObjectType 編碼結(jié)構(gòu)類(lèi)型,AAC-LC為2 二進(jìn)制位00010
//4bit samplingFrequencyIndex 音頻采樣索引值,44100對(duì)應(yīng)值是4,二進(jìn)制位0100
//4bit channelConfiguration 音頻輸出聲道,對(duì)應(yīng)的值是2,二進(jìn)制位0010
//1bit frameLengthFlag 標(biāo)志位用于表明IMDCT窗口長(zhǎng)度 0 二進(jìn)制位0
//1bit dependsOnCoreCoder 標(biāo)志位,表面是否依賴(lài)與corecoder 0 二進(jìn)制位0
//1bit extensionFlag 選擇了AAC-LC,這里必須是0 二進(jìn)制位0
//上面都合成二進(jìn)制0001001000010000
uint16_t audioConfig = 0 ;
//這里的2表示對(duì)應(yīng)的是AAC-LC 由于是5個(gè)bit,左移11位,變?yōu)?6bit,2個(gè)字節(jié)
//與上一個(gè)1111100000000000(0xF800),即只保留前5個(gè)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,二進(jìn)制位0000001000000000 & 0000011110000000(0x0780)(只保留5bit后4位)
audioConfig |= ((sampleRateIndex << 7) & 0x0780) ;
//sampleRateIndex為4,二進(jìn)制位000000000000000 & 0000000001111000(0x78)(只保留5+4后4位)
audioConfig |= ((channel << 3) & 0x78) ;
//最后三個(gè)bit都為0保留最后三位111(0x07)
audioConfig |= (0 & 0x07) ;
//最后得到合成后的數(shù)據(jù)0001001000010000,然后分別取這兩個(gè)字節(jié)
body[2] = ( audioConfig >> 8 ) & 0xFF ;
body[3] = ( audioConfig & 0xFF );
至此,我們就分別構(gòu)造了AVC sequence header 和AAC sequence header,這兩個(gè)結(jié)構(gòu)是推流的先決條件,沒(méi)有這兩個(gè)東西,解碼器是無(wú)法解碼的,最后我們?cè)賮?lái)看看我們把解碼的音視頻如何rtmp推送
/**
* 發(fā)送H264數(shù)據(jù)
* @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;
//當(dāng)NAL頭信息中,type(5位)等于5,說(shuō)明這是關(guān)鍵幀NAL單元
//buf[0] NAL Header與運(yùn)算,獲取type,根據(jù)type判斷關(guān)鍵幀和普通幀
//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;
//寫(xiě)入NALU信息,右移8位,一個(gè)字節(jié)的讀取
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;
//當(dāng)前packet的類(lèi)型: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;
//記錄了每一個(gè)tag相對(duì)于第一個(gè)tag(File Header)的相對(duì)時(shí)間
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);
}
/**
* 發(fā)送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數(shù)據(jù)*/
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都是調(diào)用的libRtmp庫(kù)的RTMP_SendPacket接口,先判斷是否rtmp是通的,是的話推流即可,最后,我們看看rtmp是如何連接服務(wù)器的:
/**
* 初始化RTMP數(shù)據(jù),與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推流的整個(gè)過(guò)程。
如果你對(duì)音視頻開(kāi)發(fā)感興趣,覺(jué)得文章對(duì)您有幫助,別忘了點(diǎn)贊、收藏哦!或者對(duì)本文的一些闡述有自己的看法,有任何問(wèn)題,歡迎在下方評(píng)論區(qū)與我討論!