本文将详细的来给大家先容RTMP推流事理以及如何推送到做事器,首先我们理解一下推流的全过程:
我们将会分为几个小节来展开:
一. 本文用到的库文件:1.1 本项目用到的库文件如下图所示,用到了ffmpeg库,以及编码视频的x264,编码音频的fdk-aac,推流利用的rtmp等:
利用静态链接库,终极把这些.a文件打包到libstream中,Android.mk如下
LOCAL_PATH := $(call my-dir)include $(CLEAR_VARS)LOCAL_MODULE := avformatLOCAL_SRC_FILES := $(LOCAL_PATH)/lib/libavformat.ainclude $(PREBUILT_STATIC_LIBRARY)include $(CLEAR_VARS)LOCAL_MODULE := avcodecLOCAL_SRC_FILES := $(LOCAL_PATH)/lib/libavcodec.ainclude $(PREBUILT_STATIC_LIBRARY)include $(CLEAR_VARS)LOCAL_MODULE := swscaleLOCAL_SRC_FILES := $(LOCAL_PATH)/lib/libswscale.ainclude $(PREBUILT_STATIC_LIBRARY)include $(CLEAR_VARS)LOCAL_MODULE := avutilLOCAL_SRC_FILES := $(LOCAL_PATH)/lib/libavutil.ainclude $(PREBUILT_STATIC_LIBRARY)include $(CLEAR_VARS)LOCAL_MODULE := swresampleLOCAL_SRC_FILES := $(LOCAL_PATH)/lib/libswresample.ainclude $(PREBUILT_STATIC_LIBRARY)include $(CLEAR_VARS)LOCAL_MODULE := postprocLOCAL_SRC_FILES := $(LOCAL_PATH)/lib/libpostproc.ainclude $(PREBUILT_STATIC_LIBRARY)include $(CLEAR_VARS)LOCAL_MODULE := x264LOCAL_SRC_FILES := $(LOCAL_PATH)/lib/libx264.ainclude $(PREBUILT_STATIC_LIBRARY)include $(CLEAR_VARS)LOCAL_MODULE := libyuvLOCAL_SRC_FILES := $(LOCAL_PATH)/lib/libyuv.ainclude $(PREBUILT_STATIC_LIBRARY)include $(CLEAR_VARS)LOCAL_MODULE := libfdk-aac#LOCAL_C_INCLUDES += $(LOCAL_PATH)/include/LOCAL_SRC_FILES := $(LOCAL_PATH)/lib/libfdk-aac.ainclude $(PREBUILT_STATIC_LIBRARY)include $(CLEAR_VARS)LOCAL_MODULE := polarsslLOCAL_SRC_FILES := $(LOCAL_PATH)/lib/libpolarssl.ainclude $(PREBUILT_STATIC_LIBRARY)include $(CLEAR_VARS)LOCAL_MODULE := rtmpLOCAL_SRC_FILES := $(LOCAL_PATH)/lib/librtmp.ainclude $(PREBUILT_STATIC_LIBRARY)include $(CLEAR_VARS)LOCAL_MODULE := libstreamLOCAL_SRC_FILES := StreamProcess.cpp FrameEncoder.cpp AudioEncoder.cpp wavreader.c RtmpLivePublish.cppLOCAL_C_INCLUDES += $(LOCAL_PATH)/include/LOCAL_STATIC_LIBRARIES := libyuv avformat avcodec swscale avutil swresample postproc x264 libfdk-aac polarssl rtmpLOCAL_LDLIBS += -L$(LOCAL_PATH)/prebuilt/ -llog -lz -Ipthreadinclude $(BUILD_SHARED_LIBRARY)
详细利用到哪些库中的接口我们将再下面进行细节展示。
【干系学习资料推举,点击下方链接免费报名,报名后会弹出学习资料免费领取地址~】
【免费】FFmpeg/WebRTC/RTMP/NDK/Android音视频流媒体高等开拓-学习视频教程-腾讯教室
C++音视频更多学习资料:点击莬费领取→音视频开拓(资料文档+视频教程+口试题)(FFmpeg+WebRTC+RTMP+RTSP+HLS+RTP)
二 . 如何从Camera摄像头获取视频流:2.1 Camera获取视频流,这个就不用多说了,只须要看到这个回调就行了,我们须要获取到这个数据:
//CameraSurfaceView.java中 @Override public void onPreviewFrame(byte[] data, Camera camera) { camera.addCallbackBuffer(data); if (listener != null) { listener.onCallback(data); } }
//壅塞线程安全行列步队,生产者和消费者 private LinkedBlockingQueue<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(wh)VUVUVUVU(wh/2),对付NV12的格式,排列办法是YYYYYYYY(wh)UVUVUVUV(wh/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 ~ 480640是Y分量;480 640 ~ 480 640 5 / 4为U分量;480 640 5 / 4 ~ 480 640 3 / 2是V分量,
记住这个打算方法,等下在JNI中立时会表示出来
那么YUV420SP和YUV420P的差异在哪里呢?显然Y的排序是完备相同的,但是UV排列上事理是完备不同的,420P它是先吧U存放完后,再放V,也便是说UV是连续的,而420SP它是UV,UV这样交替存放: YUV420SP格式:
YUV420P格式:
以是NV21(YUV420SP)的数据如下: 同样的以480 640大小的图片为例,其字节数为 480 640 3 >> 1个字节 Y分量:480 640个字节 UV分量:480 640 >>1个字节(把稳,我们没有把UV分量分开) 加起来就为480 640 3 >> 1个字节
【干系学习资料推举,点击下方链接免费报名,报名后会弹出学习资料免费领取地址~】
【免费】FFmpeg/WebRTC/RTMP/NDK/Android音视频流媒体高等开拓-学习视频教程-腾讯教室
下面我们来看看两个JNI函数,这个是摄像头转化的两个最关键的函数
/ NV21转化为YUV420P数据 @param src 原始数据 @param width 原始数据宽度 @param height 原始数据高度 @param dst 天生数据 @param dst_width 天生数据宽度 @param dst_height 天生数据高度 @param mode 模式 @param degree 角度 @param isMirror 是否镜像 @return / public static native int compressYUV(byte[] src, int width, int height, byte[] dst, int dst_width, int dst_height, int mode, int degree, boolean isMirror); / YUV420P数据的裁剪 @param src 原始数据 @param width 原始数据宽度 @param height 原始数据高度 @param dst 天生数据 @param dst_width 天生数据宽度 @param dst_height 天生数据高度 @param left 裁剪的起始x点 @param top 裁剪的起始y点 @return / public static native int cropYUV(byte[] src, int width, int height, byte[] dst, int dst_width, int dst_height, int left, int top);
再看一看详细实现
JNIEXPORT jint JNICALL Java_com_riemannlee_liveproject_StreamProcessManager_compressYUV (JNIEnv env, jclass type, jbyteArray src_, jint width, jint height, jbyteArray dst_, jint dst_width, jint dst_height, jint mode, jint degree, jboolean isMirror) { jbyte Src_data = env->GetByteArrayElements(src_, NULL); jbyte Dst_data = env->GetByteArrayElements(dst_, NULL); //nv21转化为i420(标准YUV420P数据) 这个temp_i420_data大小是和Src_data是一样的 nv21ToI420(Src_data, width, height, temp_i420_data); //进行缩放的操作,这个缩放,会把数据压缩 scaleI420(temp_i420_data, width, height, temp_i420_data_scale, dst_width, dst_height, mode); //如果是前置摄像头,进行镜像操作 if (isMirror) { //进行旋转的操作 rotateI420(temp_i420_data_scale, dst_width, dst_height, temp_i420_data_rotate, degree); //由于旋转的角度都是90和270,那后面的数据width和height是相反的 mirrorI420(temp_i420_data_rotate, dst_height, dst_width, Dst_data); } else { //进行旋转的操作 rotateI420(temp_i420_data_scale, dst_width, dst_height, Dst_data, degree); } env->ReleaseByteArrayElements(dst_, Dst_data, 0); env->ReleaseByteArrayElements(src_, Src_data, 0); return 0;}
我们从java层通报过来的参数可以看到,原始数据是1080 1920,先转为1080 1920的标准的YUV420P的数据,下面的代码便是上面我举的例子,如何拆分YUV420P的Y,U,V分量和如何拆分YUV420SP的Y,UV分量,末了调用libyuv库的libyuv::NV21ToI420数据就完成了转换;然后进行缩放,调用了libyuv::I420Scale的函数完成转换
//NV21转化为YUV420P数据void nv21ToI420(jbyte src_nv21_data, jint width, jint height, jbyte src_i420_data) { //Y通道数据大小 jint src_y_size = width height; //U通道数据大小 jint src_u_size = (width >> 1) (height >> 1); //NV21中Y通道数据 jbyte src_nv21_y_data = src_nv21_data; //由于是连续存储的Y通道数据后即为VU数据,它们的存储办法是交叉存储的 jbyte src_nv21_vu_data = src_nv21_data + src_y_size; //YUV420P中Y通道数据 jbyte src_i420_y_data = src_i420_data; //YUV420P中U通道数据 jbyte src_i420_u_data = src_i420_data + src_y_size; //YUV420P中V通道数据 jbyte src_i420_v_data = src_i420_data + src_y_size + src_u_size; //直接调用libyuv中接口,把NV21数据转化为YUV420P标准数据,此时,它们的存储大小是不变的 libyuv::NV21ToI420((const uint8 ) src_nv21_y_data, width, (const uint8 ) src_nv21_vu_data, width, (uint8 ) src_i420_y_data, width, (uint8 ) src_i420_u_data, width >> 1, (uint8 ) src_i420_v_data, width >> 1, width, height);}//进行缩放操作,此时是把1080 1920的YUV420P的数据 ==> 480 640的YUV420P的数据void scaleI420(jbyte src_i420_data, jint width, jint height, jbyte dst_i420_data, jint dst_width, jint dst_height, jint mode) { //Y数据大小widthheight,U数据大小为1/4的widthheight,V大小和U一样,一共是3/2的widthheight大小 jint src_i420_y_size = width height; jint src_i420_u_size = (width >> 1) (height >> 1); //由于是标准的YUV420P的数据,我们可以把三个通道全部分离出来 jbyte src_i420_y_data = src_i420_data; jbyte src_i420_u_data = src_i420_data + src_i420_y_size; jbyte src_i420_v_data = src_i420_data + src_i420_y_size + src_i420_u_size; //由于是标准的YUV420P的数据,我们可以把三个通道全部分离出来 jint dst_i420_y_size = dst_width dst_height; jint dst_i420_u_size = (dst_width >> 1) (dst_height >> 1); jbyte dst_i420_y_data = dst_i420_data; jbyte dst_i420_u_data = dst_i420_data + dst_i420_y_size; jbyte dst_i420_v_data = dst_i420_data + dst_i420_y_size + dst_i420_u_size; //调用libyuv库,进行缩放操作 libyuv::I420Scale((const uint8 ) src_i420_y_data, width, (const uint8 ) src_i420_u_data, width >> 1, (const uint8 ) src_i420_v_data, width >> 1, width, height, (uint8 ) dst_i420_y_data, dst_width, (uint8 ) dst_i420_u_data, dst_width >> 1, (uint8 ) dst_i420_v_data, dst_width >> 1, dst_width, dst_height, (libyuv::FilterMode) mode);}
至此,我们就把摄像头的NV21数据转化为YUV420P的标准数据了,这样,我们就可以把这个数据流转化为H264了,接下来,我们来看看如何把YUV420P流数据转化为h264数据,从而为推流做准备
三 标准YUV420P数据编码为H264多说无用,直接上代码
3.1 代码如何实现h264编码的:
/ 编码类MediaEncoder,紧张是把视频流YUV420P格式编码为h264格式,把PCM裸音频转化为AAC格式 /public class MediaEncoder { private static final String TAG = "MediaEncoder"; private Thread videoEncoderThread, audioEncoderThread; private boolean videoEncoderLoop, audioEncoderLoop; //视频流行列步队 private LinkedBlockingQueue<VideoData> videoQueue; //音频流行列步队 private LinkedBlockingQueue<AudioData> audioQueue;.........//摄像头的YUV420P数据,put到行列步队中,生产者模型 public void putVideoData(VideoData videoData) { try { videoQueue.put(videoData); } catch (InterruptedException e) { e.printStackTrace(); } }......... videoEncoderThread = new Thread() { @Override public void run() { //视频消费者模型,不断从行列步队中取出视频流来进行h264编码 while (videoEncoderLoop && !Thread.interrupted()) { try { //行列步队中取视频数据 VideoData videoData = videoQueue.take(); fps++; byte[] outbuffer = new byte[videoData.width videoData.height]; int[] buffLength = new int[10]; //对YUV420P进行h264编码,返回一个数据大小,里面是编码出来的h264数据 int numNals = StreamProcessManager.encoderVideoEncode(videoData.videoData, videoData.videoData.length, fps, outbuffer, buffLength); //Log.e("RiemannLee", "data.length " + videoData.videoData.length + " h264 encode length " + buffLength[0]); if (numNals > 0) { int[] segment = new int[numNals]; System.arraycopy(buffLength, 0, segment, 0, numNals); int totalLength = 0; for (int i = 0; i < segment.length; i++) { totalLength += segment[i]; } //Log.i("RiemannLee", "###############totalLength " + totalLength); //编码后的h264数据 byte[] encodeData = new byte[totalLength]; System.arraycopy(outbuffer, 0, encodeData, 0, encodeData.length); if (sMediaEncoderCallback != null) { sMediaEncoderCallback.receiveEncoderVideoData(encodeData, encodeData.length, segment); } //我们可以把数据在java层保存到文件中,看看我们编码的h264数据是否能播放,h264裸数据可以在VLC播放器中播放 if (SAVE_FILE_FOR_TEST) { videoFileManager.saveFileData(encodeData); } } } catch (InterruptedException e) { e.printStackTrace(); break; } } } }; videoEncoderLoop = true; videoEncoderThread.start(); }
至此,我们就把摄像头的NV21数据转化为YUV420P的标准数据了,这样,我们就可以把这个数据流转化为H264了,接下来,我们来看看如何把YUV420P流数据转化为h264数据,从而为推流做准备
三 标准YUV420P数据编码为H264多说无用,直接上代码
3.1 代码如何实现h264编码的:
/ 编码类MediaEncoder,紧张是把视频流YUV420P格式编码为h264格式,把PCM裸音频转化为AAC格式 /public class MediaEncoder { private static final String TAG = "MediaEncoder"; private Thread videoEncoderThread, audioEncoderThread; private boolean videoEncoderLoop, audioEncoderLoop; //视频流行列步队 private LinkedBlockingQueue<VideoData> videoQueue; //音频流行列步队 private LinkedBlockingQueue<AudioData> audioQueue;.........//摄像头的YUV420P数据,put到行列步队中,生产者模型 public void putVideoData(VideoData videoData) { try { videoQueue.put(videoData); } catch (InterruptedException e) { e.printStackTrace(); } }......... videoEncoderThread = new Thread() { @Override public void run() { //视频消费者模型,不断从行列步队中取出视频流来进行h264编码 while (videoEncoderLoop && !Thread.interrupted()) { try { //行列步队中取视频数据 VideoData videoData = videoQueue.take(); fps++; byte[] outbuffer = new byte[videoData.width videoData.height]; int[] buffLength = new int[10]; //对YUV420P进行h264编码,返回一个数据大小,里面是编码出来的h264数据 int numNals = StreamProcessManager.encoderVideoEncode(videoData.videoData, videoData.videoData.length, fps, outbuffer, buffLength); //Log.e("RiemannLee", "data.length " + videoData.videoData.length + " h264 encode length " + buffLength[0]); if (numNals > 0) { int[] segment = new int[numNals]; System.arraycopy(buffLength, 0, segment, 0, numNals); int totalLength = 0; for (int i = 0; i < segment.length; i++) { totalLength += segment[i]; } //Log.i("RiemannLee", "###############totalLength " + totalLength); //编码后的h264数据 byte[] encodeData = new byte[totalLength]; System.arraycopy(outbuffer, 0, encodeData, 0, encodeData.length); if (sMediaEncoderCallback != null) { sMediaEncoderCallback.receiveEncoderVideoData(encodeData, encodeData.length, segment); } //我们可以把数据在java层保存到文件中,看看我们编码的h264数据是否能播放,h264裸数据可以在VLC播放器中播放 if (SAVE_FILE_FOR_TEST) { videoFileManager.saveFileData(encodeData); } } } catch (InterruptedException e) { e.printStackTrace(); break; } } } }; videoEncoderLoop = true; videoEncoderThread.start(); }
这个便是如何把YUV420P数据转化为h264流,紧张代码是这个JNI函数,接下来我们看是如何编码成h264的,编码函数如下:
/ 编码视频数据接口 @param srcFrame 原始数据(YUV420P数据) @param frameSize 帧大小 @param fps fps @param dstFrame 编码后的数据存储 @param outFramewSize 编码后的数据大小 @return / public static native int encoderVideoEncode(byte[] srcFrame, int frameSize, int fps, byte[] dstFrame, int[] outFramewSize);
JNI中视频流的编码接口,我们看到的是初始化一个FrameEncoder类,然后调用这个类的encodeFrame接口去编码
//初始化视频编码JNIEXPORT jint JNICALL Java_com_riemannlee_liveproject_StreamProcessManager_encoderVideoinit (JNIEnv env, jclass type, jint jwidth, jint jheight, jint joutwidth, jint joutheight){ frameEncoder = new FrameEncoder(); frameEncoder->setInWidth(jwidth); frameEncoder->setInHeight(jheight); frameEncoder->setOutWidth(joutwidth); frameEncoder->setOutHeight(joutheight); frameEncoder->setBitrate(128); frameEncoder->open(); return 0;}//视频编码紧张函数,把稳JNI函数GetByteArrayElements和ReleaseByteArrayElements成对涌现,否则回内存透露JNIEXPORT jint JNICALL Java_com_riemannlee_liveproject_StreamProcessManager_encoderVideoEncode (JNIEnv env, jclass type, jbyteArray jsrcFrame, jint jframeSize, jint counter, jbyteArray jdstFrame, jintArray jdstFrameSize){ jbyte Src_data = env->GetByteArrayElements(jsrcFrame, NULL); jbyte Dst_data = env->GetByteArrayElements(jdstFrame, NULL); jint dstFrameSize = env->GetIntArrayElements(jdstFrameSize, NULL); int numNals = frameEncoder->encodeFrame((char)Src_data, jframeSize, counter, (char)Dst_data, dstFrameSize); env->ReleaseByteArrayElements(jdstFrame, Dst_data, 0); env->ReleaseByteArrayElements(jsrcFrame, Src_data, 0); env->ReleaseIntArrayElements(jdstFrameSize, dstFrameSize, 0); return numNals;}
下面我们来详细的剖析FrameEncoder这个C++类,这里我们用到了多个库,第一个便是鼎鼎大名的ffmpeg库,还有便是X264库,下面我们先来理解一下h264的文件构造,这样有利于我们理解h264的编码流程
3.2 h264我们必须知道的一些观点:
首先我们来先容h264字节流,先来理解下面几个观点,h264由哪些东西组成呢?1.VCL video coding layer 视频编码层;2.NAL network abstraction layer 网络提取层;个中,VCL层是对核心算法引擎,块,宏块及片的语法级别的定义,他终极输出编码完的数据 SODBSODB:String of Data Bits,数据比特串,它是最原始的编码数据RBSP:Raw Byte Sequence Payload,原始字节序载荷,它是在SODB的后面添加了却尾比特和多少比特0,以便字节对齐EBSP:Encapsulate Byte Sequence Payload,扩展字节序列载荷,它是在RBSP根本上添加了防校验字节0x03后得到的。关系大致如下:SODB + RBSP STOP bit + 0bits = RBSPRBSP part1+0x03+RBSP part2+0x03+…+RBSP partn = EBSPNALU Header+EBSP=NALU(NAL单元)start code+NALU+…+start code+NALU=H.264 Byte Stream
NALU头构造
长度:1byte(1个字节)forbidden_bit(1bit) + nal_reference_bit(2bit) + nal_unit_type(5bit)1. forbidden_bit:禁止位,初始为0,当网络创造NAL单元有比特缺点时可设置该比特为1,以便吸收方纠错或丧失落该单元。2. nal_reference_bit:nal主要性指示,标志该NAL单元的主要性,值越大,越主要,解码器在解码处理不过来的时候,可以丧失落主要性为0的NALU。
NALU类型构造图:
个中,nal_unit_type为1, 2, 3, 4, 5及12的NAL单元称为VCL的NAL单元,其他类型的NAL单元为非VCL的NAL单元。
【干系学习资料推举,点击下方链接免费报名,报名后会弹出学习资料免费领取地址~】
【免费】FFmpeg/WebRTC/RTMP/NDK/Android音视频流媒体高等开拓-学习视频教程-腾讯教室
C++音视频更多学习资料:点击莬费领取→音视频开拓(资料文档+视频教程+口试题)(FFmpeg+WebRTC+RTMP+RTSP+HLS+RTP)
对应的代码定义如下
public static final int NAL_UNKNOWN = 0; public static final int NAL_SLICE = 1; / 非关键帧 / public static final int NAL_SLICE_DPA = 2; public static final int NAL_SLICE_DPB = 3; public static final int NAL_SLICE_DPC = 4; public static final int NAL_SLICE_IDR = 5; / 关键帧 / public static final int NAL_SEI = 6; public static final int NAL_SPS = 7; / SPS / public static final int NAL_PPS = 8; / PPS / public static final int NAL_AUD = 9; public static final int NAL_FILLER = 12;
由上面我们可以知道,h264字节流,便是由一些start code + NALU组成的,要组成一个NALU单元,首先要有原始数据,称之为SODB,它是原始的H264数据编码得到到,不包括3字节(0x000001)/4字节(0x00000001)的start code,也不会包括1字节的NALU头, NALU头部信息包括了一些根本信息,比如NALU类型。 ps:起始码包括两种,3字节0x000001和4字节0x00000001,在sps和pps和Access Unit的第一个NALU利用4字节起始码,别的情形均利用3字节起始码
在 H264 SPEC 中,RBSP 定义如下: 在SODB结束处添加表示结束的bit 1来表示SODB已经结束,因此添加的bit 1成为rbsp_stop_one_bit,RBSP也须要字节对齐,为此须要在rbsp_stop_one_bit后添加多少0补齐,大略来说,要在SODB后面追加两样东西就形成了RBSP rbsp_stop_one_bit = 1 rbsp_alignment_zero_bit(s) = 0(s)
RBSP的天生过程:
即RBSP末了一个字节包含SODB末了几个比特,以及trailing bits个中,第一个比特位1,别的的比特位0,担保字节对齐,末了再结尾处添加0x0000,即CABAC_ZERO_WORD,从而形成 RBSP。
EBSP的天生过程:NALU数据+起始码就形成了AnnexB格式(下面有先容H264的两种格式,AnnexB为常用的格式),起始码包括两种,0x000001和0x00000001,为了不让NALU的主体和起始码之间产生竞争,在对RBSP进行扫描的时候,如果碰着连续两个0x00字节,则在该两个字节后面添加一个0x03字节,在解码的时候将该0x03字节去掉,也称为脱壳操作。解码器在解码时,首先逐个字节读取NAL的数据,统计NAL的长度,然后再开始解码。 更换规则如下: 0x000000 => 0x00000300 0x000001 => 0x00000301 0x000002 => 0x00000302 0x000003 => 0x00000303
3.3 下面我们找一个h264文件来看看
00 00 00 01 67 ... 这个为SPS,67为NALU Header,有type信息,后面即为我们说的EBSP00 00 00 01 68 ... 这个为PPS00 00 01 06 ... 为SEI补充增强信息00 00 01 65... 为IDR关键帧,图像中的编码slice
对付这个SPS凑集,从67type后开始打算, 即42 c0 33 a6 80 b4 1e 68 40 00 00 03 00 40 00 00 0c a3 c6 0c a8 正如前面的描述,解码的时候直接03 这个03是竞争检测
从前面我们剖析知道,VCL层出来的是编码完的视频帧数据,这些帧可能是I,B,P帧,而且这些帧可能属于不同的序列,在这同一个序列还有相对应的一套序列参数集和图片参数集,以是要完成视频的解码,不仅须要传输VCL层编码出来的视频帧数据,还须要传输序列参数集,图像参数集等数据。
参数集:包括序列参数集SPS和图像参数集PPS
SPS:包含的是针对持续续编码视频序列的参数,如标识符seq_parameter_set_id,帧数以及POC的约束,参数帧数目,解码图像尺寸和帧场编码模式选择标识等等 PPS:对应的是一个序列中某一副图像或者某几幅图像,其参数如标识符pic_parameter_set_id、可选的 seq_parameter_set_id、熵编码模式选择标识,片组数目,初始量化参数和去方块滤波系数调度标识等等 数据分割:组成片的编码数据存放在3个独立的DP(数据分割A,B,C)中,各自包含一个编码片的子集, 分割A包含片头和片中宏块头数据 分割B包含帧内和 SI 片宏块的编码残差数据。 分割 C包含帧间宏块的编码残差数据。 每个分割可放在独立的 NAL 单元并独立传输。
NALU的顺序哀求 H264/AVC标准对送到解码器的NAL单元是由严格哀求的,如果NAL单元的顺序是混乱的,必须将其重新依照规范组织后送入解码器,否则不能精确解码
1. 序列参数集NAL单元必须在传送所有以此参数集为参考的其它NAL单元之前传送,不过许可这些NAL单元中中间涌现重复的序列参数凑集NAL单元。所谓重复的详细阐明为:序列参数集NAL单元都有其专门的标识,如果两个序列参数集NAL单元的标识相同,就可以认为后一个只不过是前一个的拷贝,而非新的序列参数集2. 图像参数集NAL单元必须在所有此参数集为参考的其它NAL单元之前传送,不过许可这些NAL单元中间涌现重复的图像参数集NAL单元,这一点与上述的序列参数集NAL单元是相同的。3. 不同基本编码图像中的片段(slice)单元和数据划分片段(data partition)单元在顺序上不可以相互交叉,即不许可属于某一基本编码图像的一系列片段(slice)单元和数据划分片段(data partition)单元中忽然涌现另一个基本编码图像的片段(slice)单元片段和数据划分片段(data partition)单元。4. 参考图像的影响:如果一幅图像以另一幅图像为参考,则属于前者的所有片段(slice)单元和数据划分片段(data partition)单元必须在属于后者的片段和数据划分片段之后,无论是基本编码图像还是冗余编码图像都必须遵守这个规则。5. 基本编码图像的所有片段(slice)单元和数据划分片段(data partition)单元必须在属于相应冗余编码图像的片段(slice)单元和数据划分片段(data partition)单元之前。6. 如果数据流中涌现了连续的无参考基本编码图像,则图像序号小的在前面。7. 如果arbitrary_slice_order_allowed_flag置为1,一个基本编码图像中的片段(slice)单元和数据划分片段(data partition)单元的顺序是任意的,如果arbitrary_slice_order_allowed_flag置为零,则要按照片段中第一个宏块的位置来确定片段的顺序,若利用数据划分,则A类数据划分片段在B类数据划分片段之前,B类数据划分片段在C类数据划分片段之前,而且对应不同片段的数据划分片段不能相互交叉,也不能与没有数据划分的片段相互交叉。8. 如果存在SEI(补充增强信息)单元的话,它必须在它所对应的基本编码图像的片段(slice)单元和数据划分片段(data partition)单元之前,并同时必须紧接在上一个基本编码图像的所有片段(slice)单元和数据划分片段(data partition)单元后边。如果SEI属于多个基本编码图像,其顺序仅以第一个基本编码图像为参照。9. 如果存在图像分割符的话,它必须在所有SEI 单元、基本编码图像的所有片段slice)单元和数据划分片段(data partition)单元之前,并且紧接着上一个基本编码图像那些NAL单元。10. 如果存在序列结束符,且序列结束符后还有图像,则该图像必须是IDR(即时解码器刷新)图像。序列结束符的位置应该在属于这个IDR图像的分割符、SEI 单元等数据之前,且紧接着前面那些图像的NAL单元。如果序列结束符后没有图像了,那么它的就在比特流中所有图像数据之后。11. 流结束符在比特流中的末了。
h264有两种封装, 一种是Annexb模式,传统模式,有startcode,SPS和PPS是在ES中 一种是mp4模式,一样平常mp4 mkv会有,没有startcode,SPS和PPS以及其它信息被封装在container中,每一个frame前面是这个frame的长度 很多解码器只支持annexb这种模式,因此须要将mp4做转换 我们谈论的是第一种Annexb传统模式,
3.4 下面我们直接看代码,理解一下如何利用X264来编码h264文件
x264_param_default_preset():为了方便利用x264,只须要根据编码速率的哀求和视频质量的哀求选择模型,并修正部分视频参数即可x264_picture_alloc():为图像构造体x264_picture_t分配内存。x264_encoder_open():打开编码器。x264_encoder_encode():编码一帧图像。x264_encoder_close():关闭编码器。x264_picture_clean():开释x264_picture_alloc()申请的资源。 存储数据的构造体如下所示。x264_picture_t:存储压缩编码前的像素数据。x264_nal_t:存储压缩编码后的码流数据。下面先容几个主要的构造体/ x264_image_t 构造用于存放一帧图像实际像素数据。该构造体定义在x264.h中/typedef struct{ int i_csp; // 设置彩色空间,常日取值 X264_CSP_I420,所有可能取值定义在x264.h中 int i_plane; // 图像平面个数,例如彩色空间是YUV420格式的,此处取值3 int i_stride[4]; // 每个图像平面的跨度,也便是每一行数据的字节数 uint8_t plane[4]; // 每个图像平面存放数据的起始地址, plane[0]是Y平面, // plane[1]和plane[2]分别代表U和V平面} x264_image_t;/x264_picture_t 构造体描述视频帧的特色,该构造体定义在x264.h中。/typedef struct{int i_type; // 帧的类型,取值有X264_TYPE_KEYFRAME X264_TYPE_P // X264_TYPE_AUTO等。初始化为auto,则在编码过程自行掌握。int i_qpplus1; // 此参数减1代表当前帧的量化参数值int i_pic_struct; // 帧的构造类型,表示是帧还是场,是逐行还是隔行, // 取值为列举值 pic_struct_e,定义在x264.h中int b_keyframe; // 输出:是否是关键帧int64_t i_pts; // 一帧的显示韶光戳int64_t i_dts; // 输出:解码韶光戳。当一帧的pts非常靠近0时,该dts值可能为负。/ 编码器参数设置,如果为NULL则表示连续利用前一帧的设置。某些参数 (例如aspect ratio) 由于收到H264本身的限定,只能每隔一个GOP才能改变。 这种情形下,如果想让这些改变的参数立即生效,则必须逼迫天生一个IDR帧。/ x264_param_t param;x264_image_t img; // 存放一帧图像的真实数据x264_image_properties_t prop;x264_hrd_t hrd_timing;// 输出:HRD韶光信息,仅当i_nal_hrd设置了才有效void opaque; // 私有数据存放区,将输入数据拷贝到输出帧中} x264_picture_t ;/x264_nal_t中的数据不才一次调用x264_encoder_encode之后就无效了,因此必须在调用x264_encoder_encode 或 x264_encoder_headers 之前利用或拷贝个中的数据。/typedef struct{int i_ref_idc; // Nal的优先级int i_type; // Nal的类型int b_long_startcode; // 是否采取长前缀码0x00000001int i_first_mb; // 如果Nal为一条带,则表示该条带第一个宏块的指数int i_last_mb; // 如果Nal为一条带,则表示该条带末了一个宏块的指数int i_payload; // payload 的字节大小uint8_t p_payload; // 存放编码后的数据,已经封装成Nal单元} x264_nal_t;
再来看看编码h264源码
//初始化视频编码JNIEXPORT jint JNICALL Java_com_riemannlee_liveproject_StreamProcessManager_encoderVideoinit (JNIEnv env, jclass type, jint jwidth, jint jheight, jint joutwidth, jint joutheight){ frameEncoder = new FrameEncoder(); frameEncoder->setInWidth(jwidth); frameEncoder->setInHeight(jheight); frameEncoder->setOutWidth(joutwidth); frameEncoder->setOutHeight(joutheight); frameEncoder->setBitrate(128); frameEncoder->open(); return 0;}FrameEncoder.cpp 源文件//供测试文件利用,测试的时候打开//#define ENCODE_OUT_FILE_1//供测试文件利用//#define ENCODE_OUT_FILE_2FrameEncoder::FrameEncoder() : in_width(0), in_height(0), out_width( 0), out_height(0), fps(0), encoder(NULL), num_nals(0) {#ifdef ENCODE_OUT_FILE_1 const char outfile1 = "/sdcard/2222.h264"; out1 = fopen(outfile1, "wb");#endif#ifdef ENCODE_OUT_FILE_2 const char outfile2 = "/sdcard/3333.h264"; out2 = fopen(outfile2, "wb");#endif}bool FrameEncoder::open() { int r = 0; int nheader = 0; int header_size = 0; if (!validateSettings()) { return false; } if (encoder) { LOGI("Already opened. first call close()"); return false; } // set encoder parameters setParams(); //按照色度空间分配内存,即为图像构造体x264_picture_t分配内存,并返回内存的首地址作为指针 //i_csp(图像颜色空间参数,目前只支持I420/YUV420)为X264_CSP_I420 x264_picture_alloc(&pic_in, params.i_csp, params.i_width, params.i_height); //create the encoder using our params 打开编码器 encoder = x264_encoder_open(¶ms); if (!encoder) { LOGI("Cannot open the encoder"); close(); return false; } // write headers r = x264_encoder_headers(encoder, &nals, &nheader); if (r < 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(); }
【干系学习资料推举,点击下方链接免费报名,报名后会弹出学习资料免费领取地址~】
【免费】FFmpeg/WebRTC/RTMP/NDK/Android音视频流媒体高等开拓-学习视频教程-腾讯教室
C++音视频更多学习资料:点击莬费领取→音视频开拓(资料文档+视频教程+口试题)(FFmpeg+WebRTC+RTMP+RTSP+HLS+RTP)
进入audio的jni编码
//音频初始化JNIEXPORT jint JNICALL Java_com_riemannlee_liveproject_StreamProcessManager_encoderAudioInit (JNIEnv env, jclass type, jint jsampleRate, jint jchannels, jint jbitRate){ audioEncoder = new AudioEncoder(jchannels, jsampleRate, jbitRate); int value = audioEncoder->init(); return value;}
现在,我们进入了AudioEncoder,进入了音频编码的天下
AudioEncoder::AudioEncoder(int channels, int sampleRate, int bitRate){ this->channels = channels; this->sampleRate = sampleRate; this->bitRate = bitRate;}............/ 初始化fdk-aac的参数,设置干系接口使得 @return /int AudioEncoder::init() { //打开AAC音频编码引擎,创建AAC编码句柄 if (aacEncOpen(&handle, 0, channels) != AACENC_OK) { LOGI("Unable to open fdkaac encoder\n"); return -1; } // 下面都是利用aacEncoder_SetParam设置参数 // AACENC_AOT设置为aac lc if (aacEncoder_SetParam(handle, AACENC_AOT, 2) != AACENC_OK) { LOGI("Unable to set the AOT\n"); return -1; } if (aacEncoder_SetParam(handle, AACENC_SAMPLERATE, sampleRate) != AACENC_OK) { LOGI("Unable to set the sampleRate\n"); return -1; } // AACENC_CHANNELMODE设置为双通道 if (aacEncoder_SetParam(handle, AACENC_CHANNELMODE, MODE_2) != AACENC_OK) { LOGI("Unable to set the channel mode\n"); return -1; } if (aacEncoder_SetParam(handle, AACENC_CHANNELORDER, 1) != AACENC_OK) { LOGI("Unable to set the wav channel order\n"); return 1; } if (aacEncoder_SetParam(handle, AACENC_BITRATE, bitRate) != AACENC_OK) { LOGI("Unable to set the bitrate\n"); return -1; } if (aacEncoder_SetParam(handle, AACENC_TRANSMUX, 2) != AACENC_OK) { //0-raw 2-adts LOGI("Unable to set the ADTS transmux\n"); return -1; } if (aacEncoder_SetParam(handle, AACENC_AFTERBURNER, 1) != AACENC_OK) { LOGI("Unable to set the ADTS AFTERBURNER\n"); return -1; } if (aacEncEncode(handle, NULL, NULL, NULL, NULL) != AACENC_OK) { LOGI("Unable to initialize the encoder\n"); return -1; } AACENC_InfoStruct info = { 0 }; if (aacEncInfo(handle, &info) != AACENC_OK) { LOGI("Unable to get the encoder info\n"); return -1; } //返回数据给上层,表示每次通报多少个数据最佳,这样encode效率最高 int inputSize = channels 2 info.frameLength; LOGI("inputSize = %d", inputSize); return inputSize;}
我们终于知道MediaEncoder布局函数中初始化音频数据的用意了,它会返回设备中通报多少inputSize为最佳,这样,我们每次只须要通报相应的数据,就可以使得音频效率更优化
public void startAudioEncode() { if (audioEncoderLoop) { throw new RuntimeException("必须先停滞"); } audioEncoderThread = new Thread() { @Override public void run() { byte[] outbuffer = new byte[1024]; int haveCopyLength = 0; byte[] inbuffer = new byte[audioEncodeBuffer]; while (audioEncoderLoop && !Thread.interrupted()) { try { AudioData audio = audioQueue.take(); //我们通过fdk-aac接口获取到了audioEncodeBuffer的数据,即每次编码多少数据为最优 //这里我这边的手机每次都是返回的4096即4K的数据,实在为了大略点,我们每次可以让 //MIC录取4K大小的数据,然后把录取的数据通报到AudioEncoder.cpp中取编码 //Log.e("RiemannLee", " audio.audioData.length " + audio.audioData.length + " audioEncodeBuffer " + audioEncodeBuffer); final int audioGetLength = audio.audioData.length; if (haveCopyLength < 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推流的全体过程。
如果你对音视频开拓感兴趣,以为文章对您有帮助,别忘了点赞、收藏哦!
或者对本文的一些阐述有自己的意见,有任何问题,欢迎不才方评论区与我谈论!