<返回更多

Android中使用ffmpeg编码进行rtmp推流

2022-09-27  今日头条  音视频开发老舅
加入收藏

要理解RTMP推流,我们就要知道详细原理。

本文将详细的来给大家介绍RTMP推流原理以及如何推送到服务器,首先我们了解一下推流的全过程:

 

我们将会分为几个小节来展开:

一. 本文用到的库文件:

1.1 本项目用到的库文件如下图所示,用到了ffmpeg库,以及编码视频的x264,编码音频的fdk-aac,推流使用的rtmp等:

 


 

使用静态链接库,最终把这些.a文件打包到libstream中,Android.mk如下

LOCAL_PATH := $(call my-dir)

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

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

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

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

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

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

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

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

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

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

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

include $(CLEAR_VARS)
LOCAL_MODULE    := libstream

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

include $(BUILD_SHARED_LIBRARY)

具体使用到哪些库中的接口我们将再下面进行细节展示。

 

二 . 如何从Camera摄像头获取视频流:

2.1 Camera获取视频流,这个就不用多说了,只需要看到这个回调就行了,我们需要获取到这个数据:

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

2.2 NV21转化为YUV420P数据 我们知道一般的摄像头的数据都是NV21或者是NV12,接下来我们会用到第一个编码库libyuv库,我们先来看看这个消费者怎么从NV21的数据转化为YUV的

    workThread = new Thread() {
            @Override
            public void run() {
                while (loop && !Thread.interrupted()) {
                    try {
                        //获取阻塞队列中的数据,没有数据的时候阻塞
                        byte[] srcData = mQueue.take();
                        //生成I420(YUV标准格式数据及YUV420P)目标数据,
                        //生成后的数据长度width * height * 3 / 2
                        final byte[] dstData = new byte[scaleWidth * scaleHeight * 3 / 2];
                        final int morientation = mCameraUtil.getMorientation();
                        //压缩NV21(YUV420SP)数据,元素数据位1080 * 1920,很显然
                        //这样的数据推流会很占用带宽,我们压缩成480 * 640 的YUV数据
                        //为啥要转化为YUV420P数据?因为是在为转化为H264数据在做
                        //准备,NV21不是标准的,只能先通过转换,生成标准YUV420P数据,
                        //然后把标准数据encode为H264流
                        StreamProcessManager.compressYUV(srcData, mCameraUtil.getCameraWidth(), mCameraUtil.getCameraHeight(), dstData, scaleHeight, scaleWidth, 0, morientation, morientation == 270);

                        //进行YUV420P数据裁剪的操作,测试下这个借口,
                        //我们可以对数据进行裁剪,裁剪后的数据也是I420数据,
                        //我们采用的是libyuv库文件
                        //这个libyuv库效率非常高,这也是我们用它的原因
                        final byte[] cropData = new byte[cropWidth * cropHeight * 3 / 2];
                        StreamProcessManager.cropYUV(dstData, scaleWidth, scaleHeight, cropData, cropWidth, cropHeight, cropStartX, cropStartY);

                        //自此,我们得到了YUV420P标准数据,这个过程实际上就是NV21转化为YUV420P数据
                        //注意,有些机器是NV12格式,只是数据存储不一样,我们一样可以用libyuv库的接口转化
                        if (yuvDataListener != null) {
                            yuvDataListener.onYUVDataReceiver(cropData, cropWidth, cropHeight);
                        }

                        //设置为true,我们把生成的YUV文件用播放器播放一下,看我们
                        //的数据是否有误,起调试作用
                        if (SAVE_FILE_FOR_TEST) {
                            fileManager.saveFileData(cropData);
                        }
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                        break;
                    }
                }
            }
        };

2.3 介绍一下摄像头的数据流格式

视频流的转换,android中一般摄像头的格式是NV21或者是NV12,它们都是YUV420sp的一种,那么什么是YUV格式呢?

何为YUV格式,有三个分量,Y表示明亮度,也就是灰度值,U和V则表示色度,即影像色彩饱和度,用于指定像素的颜色,(直接点就是Y是亮度信息,UV是色彩信息),YUV格式分为两大类,planar和packed两种:

对于planar的YUV格式,先连续存储所有像素点Y,紧接着存储所有像素点U,随后所有像素点V
对于packed的YUV格式,每个像素点YUV是连续交替存储的

YUV格式为什么后面还带数字呢,比如YUV 420,444,442 YUV444:每一个Y对应一组UV分量 YUV422:每两个Y共用一组UV分量 YUV420:每四个Y公用一组UV分量

实际上NV21,NV12就是属于YUV420,是一种two-plane模式,即Y和UV分为两个Plane,UV为交错存储,他们都属于YUV420SP,举个例子就会很清晰了

NV21格式数据排列方式是YYYYYYYY(w*h)VUVUVUVU(w*h/2),
对于NV12的格式,排列方式是YYYYYYYY(w*h)UVUVUVUV(w*h/2)

正如代码注释中所说的那样,我们以标准的YUV420P为例,对于这样的格式,我们要取出Y,U,V这三个分量,我们看怎么取?

比如480 * 640大小的图片,其字节数为 480 * 640 * 3 >> 1个字节
Y分量:480 * 640个字节
U分量:480 * 640 >>2个字节
V分量:480 * 640 >>2个字节,加起来就为480 * 640 * 3 >> 1个字节
存储都是行优先存储,三部分之间顺序是YUV依次存储,即
0 ~ 480*640是Y分量;480 * 640 ~ 480 * 640 * 5 / 4为U分量;480 * 640 * 5 / 4 ~ 480 * 640 * 3 / 2是V分量,

记住这个计算方法,等下在JNI中马上会体现出来

那么YUV420SP和YUV420P的区别在哪里呢?显然Y的排序是完全相同的,但是UV排列上原理是完全不同的,420P它是先吧U存放完后,再放V,也就是说UV是连续的,而420SP它是UV,UV这样交替存放: YUV420SP格式:

 

YUV420P格式:

 

所以NV21(YUV420SP)的数据如下: 同样的以480 * 640大小的图片为例,其字节数为 480 * 640 * 3 >> 1个字节 Y分量:480 * 640个字节 UV分量:480 * 640 >>1个字节(注意,我们没有把UV分量分开) 加起来就为480 * 640 * 3 >> 1个字节

下面我们来看看两个JNI函数,这个是摄像头转化的两个最关键的函数

/**
     * NV21转化为YUV420P数据
     * @param src         原始数据
     * @param width       原始数据宽度
     * @param height      原始数据高度
     * @param dst         生成数据
     * @param dst_width   生成数据宽度
     * @param dst_height  生成数据高度
     * @param mode        模式
     * @param degree      角度
     * @param isMirror    是否镜像
     * @return
     */
    public static native int compressYUV(byte[] src, int width, int height, byte[] dst, int dst_width, int dst_height, int mode, int degree, boolean isMirror);

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

再看一看具体实现

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

    jbyte *Src_data = env->GetByteArrayElements(src_, NULL);
    jbyte *Dst_data = env->GetByteArrayElements(dst_, NULL);
    //nv21转化为i420(标准YUV420P数据) 这个temp_i420_data大小是和Src_data是一样的
    nv21ToI420(Src_data, width, height, temp_i420_data);
    //进行缩放的操作,这个缩放,会把数据压缩
    scaleI420(temp_i420_data, width, height, temp_i420_data_scale, dst_width, dst_height, mode);
    //如果是前置摄像头,进行镜像操作
    if (isMirror) {
        //进行旋转的操作
        rotateI420(temp_i420_data_scale, dst_width, dst_height, temp_i420_data_rotate, degree);
        //因为旋转的角度都是90和270,那后面的数据width和height是相反的
        mirrorI420(temp_i420_data_rotate, dst_height, dst_width, Dst_data);
    } else {
        //进行旋转的操作
        rotateI420(temp_i420_data_scale, dst_width, dst_height, Dst_data, degree);
    }
    env->ReleaseByteArrayElements(dst_, Dst_data, 0);
    env->ReleaseByteArrayElements(src_, Src_data, 0);

    return 0;
}

我们从java层传递过来的参数可以看到,原始数据是1080 * 1920,先转为1080 * 1920的标准的YUV420P的数据,下面的代码就是上面我举的例子,如何拆分YUV420P的Y,U,V分量和如何拆分YUV420SP的Y,UV分量,最后调用libyuv库的libyuv::NV21ToI420数据就完成了转换;然后进行缩放,调用了libyuv::I420Scale的函数完成转换

//NV21转化为YUV420P数据
void nv21ToI420(jbyte *src_nv21_data, jint width, jint height, jbyte *src_i420_data) {
    //Y通道数据大小
    jint src_y_size = width * height;
    //U通道数据大小
    jint src_u_size = (width >> 1) * (height >> 1);

    //NV21中Y通道数据
    jbyte *src_nv21_y_data = src_nv21_data;
    //由于是连续存储的Y通道数据后即为VU数据,它们的存储方式是交叉存储的
    jbyte *src_nv21_vu_data = src_nv21_data + src_y_size;

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

    //直接调用libyuv中接口,把NV21数据转化为YUV420P标准数据,此时,它们的存储大小是不变的
    libyuv::NV21ToI420((const uint8 *) src_nv21_y_data, width,
                       (const uint8 *) src_nv21_vu_data, width,
                       (uint8 *) src_i420_y_data, width,
                       (uint8 *) src_i420_u_data, width >> 1,
                       (uint8 *) src_i420_v_data, width >> 1,
                       width, height);
}

//进行缩放操作,此时是把1080 * 1920的YUV420P的数据 ==> 480 * 640的YUV420P的数据
void scaleI420(jbyte *src_i420_data, jint width, jint height, jbyte *dst_i420_data, jint dst_width,
               jint dst_height, jint mode) {
    //Y数据大小width*height,U数据大小为1/4的width*height,V大小和U一样,一共是3/2的width*height大小
    jint src_i420_y_size = width * height;
    jint src_i420_u_size = (width >> 1) * (height >> 1);
    //由于是标准的YUV420P的数据,我们可以把三个通道全部分离出来
    jbyte *src_i420_y_data = src_i420_data;
    jbyte *src_i420_u_data = src_i420_data + src_i420_y_size;
    jbyte *src_i420_v_data = src_i420_data + src_i420_y_size + src_i420_u_size;

    //由于是标准的YUV420P的数据,我们可以把三个通道全部分离出来
    jint dst_i420_y_size = dst_width * dst_height;
    jint dst_i420_u_size = (dst_width >> 1) * (dst_height >> 1);
    jbyte *dst_i420_y_data = dst_i420_data;
    jbyte *dst_i420_u_data = dst_i420_data + dst_i420_y_size;
    jbyte *dst_i420_v_data = dst_i420_data + dst_i420_y_size + dst_i420_u_size;

    //调用libyuv库,进行缩放操作
    libyuv::I420Scale((const uint8 *) src_i420_y_data, width,
                      (const uint8 *) src_i420_u_data, width >> 1,
                      (const uint8 *) src_i420_v_data, width >> 1,
                      width, height,
                      (uint8 *) dst_i420_y_data, dst_width,
                      (uint8 *) dst_i420_u_data, dst_width >> 1,
                      (uint8 *) dst_i420_v_data, dst_width >> 1,
                      dst_width, dst_height,
                      (libyuv::FilterMode) mode);
}

至此,我们就把摄像头的NV21数据转化为YUV420P的标准数据了,这样,我们就可以把这个数据流转化为H264了,接下来,我们来看看如何把YUV420P流数据转化为h264数据,从而为推流做准备

三 标准YUV420P数据编码为H264

多说无用,直接上代码

3.1 代码如何实现h264编码的:

/**
 * 编码类MediaEncoder,主要是把视频流YUV420P格式编码为h264格式,把PCM裸音频转化为AAC格式
 */
public class MediaEncoder {
    private static final String TAG = "MediaEncoder";

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

    //视频流队列
    private LinkedBlockingQueue<VideoData> videoQueue;
    //音频流队列
    private LinkedBlockingQueue<AudioData> audioQueue;
.........

//摄像头的YUV420P数据,put到队列中,生产者模型
    public void putVideoData(VideoData videoData) {
        try {
            videoQueue.put(videoData);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
.........
 videoEncoderThread = new Thread() {
            @Override
            public void run() {
                //视频消费者模型,不断从队列中取出视频流来进行h264编码
                while (videoEncoderLoop && !Thread.interrupted()) {
                    try {
                        //队列中取视频数据
                        VideoData videoData = videoQueue.take();
                        fps++;
                        byte[] outbuffer = new byte[videoData.width * videoData.height];
                        int[] buffLength = new int[10];
                        //对YUV420P进行h264编码,返回一个数据大小,里面是编码出来的h264数据
                        int numNals = StreamProcessManager.encoderVideoEncode(videoData.videoData, videoData.videoData.length, fps, outbuffer, buffLength);
                        //Log.e("RiemannLee", "data.length " +  videoData.videoData.length + " h264 encode length " + buffLength[0]);
                        if (numNals > 0) {
                            int[] segment = new int[numNals];
                            System.arraycopy(buffLength, 0, segment, 0, numNals);
                            int totalLength = 0;
                            for (int i = 0; i < segment.length; i++) {
                                totalLength += segment[i];
                            }
                            //Log.i("RiemannLee", "###############totalLength " + totalLength);
                            //编码后的h264数据
                            byte[] encodeData = new byte[totalLength];
                            System.arraycopy(outbuffer, 0, encodeData, 0, encodeData.length);
                            if (sMediaEncoderCallback != null) {
                                sMediaEncoderCallback.receiveEncoderVideoData(encodeData, encodeData.length, segment);
                            }
                            //我们可以把数据在java层保存到文件中,看看我们编码的h264数据是否能播放,h264裸数据可以在VLC播放器中播放
                            if (SAVE_FILE_FOR_TEST) {
                                videoFileManager.saveFileData(encodeData);
                            }
                        }
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                        break;
                    }
                }

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

至此,我们就把摄像头的NV21数据转化为YUV420P的标准数据了,这样,我们就可以把这个数据流转化为H264了,接下来,我们来看看如何把YUV420P流数据转化为h264数据,从而为推流做准备

三 标准YUV420P数据编码为H264

多说无用,直接上代码

3.1 代码如何实现h264编码的:

/**
 * 编码类MediaEncoder,主要是把视频流YUV420P格式编码为h264格式,把PCM裸音频转化为AAC格式
 */
public class MediaEncoder {
    private static final String TAG = "MediaEncoder";

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

    //视频流队列
    private LinkedBlockingQueue<VideoData> videoQueue;
    //音频流队列
    private LinkedBlockingQueue<AudioData> audioQueue;
.........

//摄像头的YUV420P数据,put到队列中,生产者模型
    public void putVideoData(VideoData videoData) {
        try {
            videoQueue.put(videoData);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
.........
 videoEncoderThread = new Thread() {
            @Override
            public void run() {
                //视频消费者模型,不断从队列中取出视频流来进行h264编码
                while (videoEncoderLoop && !Thread.interrupted()) {
                    try {
                        //队列中取视频数据
                        VideoData videoData = videoQueue.take();
                        fps++;
                        byte[] outbuffer = new byte[videoData.width * videoData.height];
                        int[] buffLength = new int[10];
                        //对YUV420P进行h264编码,返回一个数据大小,里面是编码出来的h264数据
                        int numNals = StreamProcessManager.encoderVideoEncode(videoData.videoData, videoData.videoData.length, fps, outbuffer, buffLength);
                        //Log.e("RiemannLee", "data.length " +  videoData.videoData.length + " h264 encode length " + buffLength[0]);
                        if (numNals > 0) {
                            int[] segment = new int[numNals];
                            System.arraycopy(buffLength, 0, segment, 0, numNals);
                            int totalLength = 0;
                            for (int i = 0; i < segment.length; i++) {
                                totalLength += segment[i];
                            }
                            //Log.i("RiemannLee", "###############totalLength " + totalLength);
                            //编码后的h264数据
                            byte[] encodeData = new byte[totalLength];
                            System.arraycopy(outbuffer, 0, encodeData, 0, encodeData.length);
                            if (sMediaEncoderCallback != null) {
                                sMediaEncoderCallback.receiveEncoderVideoData(encodeData, encodeData.length, segment);
                            }
                            //我们可以把数据在java层保存到文件中,看看我们编码的h264数据是否能播放,h264裸数据可以在VLC播放器中播放
                            if (SAVE_FILE_FOR_TEST) {
                                videoFileManager.saveFileData(encodeData);
                            }
                        }
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                        break;
                    }
                }

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

这个就是如何把YUV420P数据转化为h264流,主要代码是这个JNI函数,接下来我们看是如何编码成h264的,编码函数如下:

    /**
     * 编码视频数据接口
     * @param srcFrame      原始数据(YUV420P数据)
     * @param frameSize     帧大小
     * @param fps           fps
     * @param dstFrame      编码后的数据存储
     * @param outFramewSize 编码后的数据大小
     * @return
     */
    public static native int encoderVideoEncode(byte[] srcFrame, int frameSize, int fps, byte[] dstFrame, int[] outFramewSize);

JNI中视频流的编码接口,我们看到的是初始化一个FrameEncoder类,然后调用这个类的encodeFrame接口去编码

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

//视频编码主要函数,注意JNI函数GetByteArrayElements和ReleaseByteArrayElements成对出现,否则回内存泄露
JNIEXPORT jint JNICALL Java_com_riemannlee_liveproject_StreamProcessManager_encoderVideoEncode
        (JNIEnv *env, jclass type, jbyteArray jsrcFrame, jint jframeSize, jint counter, jbyteArray jdstFrame, jintArray jdstFrameSize)
{
    jbyte *Src_data = env->GetByteArrayElements(jsrcFrame, NULL);
    jbyte *Dst_data = env->GetByteArrayElements(jdstFrame, NULL);
    jint *dstFrameSize = env->GetIntArrayElements(jdstFrameSize, NULL);

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

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

    return numNals;
}

下面我们来详细的分析FrameEncoder这个C++类,这里我们用到了多个库,第一个就是鼎鼎大名的ffmpeg库,还有就是X264库,下面我们先来了解一下h264的文件结构,这样有利于我们理解h264的编码流程

3.2 h264我们必须知道的一些概念:

首先我们来介绍h264字节流,先来了解下面几个概念,h264由哪些东西组成呢?
1.VCL   video coding layer    视频编码层;
2.NAL  .NETwork abstraction layer   网络提取层;
其中,VCL层是对核心算法引擎,块,宏块及片的语法级别的定义,他最终输出编码完的数据 SODB

SODB:String of Data Bits,数据比特串,它是最原始的编码数据
RBSP:Raw Byte Sequence Payload,原始字节序载荷,它是在SODB的后面添加了结尾比特和若干比特0,以便字节对齐
EBSP:Encapsulate Byte Sequence Payload,扩展字节序列载荷,它是在RBSP基础上添加了防校验字节0x03后得到的。
关系大致如下:

SODB + RBSP STOP bit + 0bits = RBSP

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

NALU Header+EBSP=NALU(NAL单元)

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

NALU头结构

长度:1byte(1个字节)
forbidden_bit(1bit) + nal_reference_bit(2bit) + nal_unit_type(5bit)
1.  forbidden_bit:禁止位,初始为0,当网络发现NAL单元有比特错误时可设置该比特为1,以便接收方纠错或
丢掉该单元。
2.  nal_reference_bit:nal重要性指示,标志该NAL单元的重要性,值越大,越重要,解码器在解码处理不过来
的时候,可以丢掉重要性为0的NALU。

NALU类型结构图:

 

其中,nal_unit_type为1, 2, 3, 4, 5及12的NAL单元称为VCL的NAL单元,其他类型的NAL单元为非VCL的NAL单元。

对应的代码定义如下

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

由上面我们可以知道,h264字节流,就是由一些start code + NALU组成的,要组成一个NALU单元,首先要有原始数据,称之为SODB,它是原始的H264数据编码得到到,不包括3字节(0x000001)/4字节(0x00000001)的start code,也不会包括1字节的NALU头, NALU头部信息包括了一些基础信息,比如NALU类型。 ps:起始码包括两种,3字节0x000001和4字节0x00000001,在sps和pps和Access Unit的第一个NALU使用4字节起始码,其余情况均使用3字节起始码

在 H264 SPEC 中,RBSP 定义如下: 在SODB结束处添加表示结束的bit 1来表示SODB已经结束,因此添加的bit 1成为rbsp_stop_one_bit,RBSP也需要字节对齐,为此需要在rbsp_stop_one_bit后添加若干0补齐,简单来说,要在SODB后面追加两样东西就形成了RBSP rbsp_stop_one_bit = 1 rbsp_alignment_zero_bit(s) = 0(s)

RBSP的生成过程:

 

即RBSP最后一个字节包含SODB最后几个比特,以及trailing bits其中,第一个比特位1,其余的比特位0,保证字节对齐,最后再结尾处添加0x0000,即CABAC_ZERO_word,从而形成 RBSP。

EBSP的生成过程:NALU数据+起始码就形成了AnnexB格式(下面有介绍H264的两种格式,AnnexB为常用的格式),起始码包括两种,0x000001和0x00000001,为了不让NALU的主体和起始码之间产生竞争,在对RBSP进行扫描的时候,如果遇到连续两个0x00字节,则在该两个字节后面添加一个0x03字节,在解码的时候将该0x03字节去掉,也称为脱壳操作。解码器在解码时,首先逐个字节读取NAL的数据,统计NAL的长度,然后再开始解码。 替换规则如下: 0x000000 => 0x00000300 0x000001 => 0x00000301 0x000002 => 0x00000302 0x000003 => 0x00000303

3.3 下面我们找一个h264文件来看看

 

00 00 00 01 67 ... 这个为SPS,67为NALU Header,有type信息,后面即为我们说的EBSP
00 00 00 01 68 ...  这个为PPS
00 00 01 06 ...  为SEI补充增强信息
00 00 01 65...  为IDR关键帧,图像中的编码slice

 

对于这个SPS集合,从67type后开始计算, 即42 c0 33 a6 80 b4 1e 68 40 00 00 03 00 40 00 00 0c a3 c6 0c a8 正如前面的描述,解码的时候直接03 这个03是竞争检测

 

从前面我们分析知道,VCL层出来的是编码完的视频帧数据,这些帧可能是I,B,P帧,而且这些帧可能属于不同的序列,在这同一个序列还有相对应的一套序列参数集和图片参数集,所以要完成视频的解码,不仅需要传输VCL层编码出来的视频帧数据,还需要传输序列参数集,图像参数集等数据。

参数集:包括序列参数集SPS和图像参数集PPS

SPS:包含的是针对一连续编码视频序列的参数,如标识符seq_parameter_set_id,帧数以及POC的约束,参数帧数目,解码图像尺寸和帧场编码模式选择标识等等 PPS:对应的是一个序列中某一副图像或者某几幅图像,其参数如标识符pic_parameter_set_id、可选的 seq_parameter_set_id、熵编码模式选择标识,片组数目,初始量化参数和去方块滤波系数调整标识等等 数据分割:组成片的编码数据存放在3个独立的DP(数据分割A,B,C)中,各自包含一个编码片的子集, 分割A包含片头和片中宏块头数据 分割B包含帧内和 SI 片宏块的编码残差数据。 分割 C包含帧间宏块的编码残差数据。 每个分割可放在独立的 NAL 单元并独立传输。

NALU的顺序要求 H264/AVC标准对送到解码器的NAL单元是由严格要求的,如果NAL单元的顺序是混乱的,必须将其重新依照规范组织后送入解码器,否则不能正确解码

1.  序列参数集NAL单元
必须在传送所有以此参数集为参考的其它NAL单元之前传送,不过允许这些NAL单元中中间出现重复的序列参数集合NAL单元。
所谓重复的详细解释为:序列参数集NAL单元都有其专门的标识,如果两个序列参数集NAL单元的标识相同,就可以认为后一个只不过是前一个的拷贝,而非新的序列参数集
2.  图像参数集NAL单元
必须在所有此参数集为参考的其它NAL单元之前传送,不过允许这些NAL单元中间出现重复的图像参数集NAL单元,这一点与上述的序列参数集NAL单元是相同的。
3.  不同基本编码图像中的片段(slice)单元和数据划分片段(data partition)单元在顺序上不可以相互交叉,即不允许属于某一基本编码图像的一系列片段(slice)单元和数据划分片段(data partition)单元中忽然出现另一个基本编码图像的片段(slice)单元片段和数据划分片段(data partition)单元。
4.  参考图像的影响:如果一幅图像以另一幅图像为参考,则属于前者的所有片段(slice)单元和数据划分片段(data partition)单元必须在属于后者的片段和数据划分片段之后,无论是基本编码图像还是冗余编码图像都必须遵守这个规则。
5.  基本编码图像的所有片段(slice)单元和数据划分片段(data partition)单元必须在属于相应冗余编码图像的片段(slice)单元和数据划分片段(data partition)单元之前。
6.  如果数据流中出现了连续的无参考基本编码图像,则图像序号小的在前面。
7.  如果arbitrary_slice_order_allowed_flag置为1,一个基本编码图像中的片段(slice)单元和数据划分片段(data partition)单元的顺序是任意的,如果arbitrary_slice_order_allowed_flag置为零,则要按照片段中第一个宏块的位置来确定片段的顺序,若使用数据划分,则A类数据划分片段在B类数据划分片段之前,B类数据划分片段在C类数据划分片段之前,而且对应不同片段的数据划分片段不能相互交叉,也不能与没有数据划分的片段相互交叉。
8.  如果存在SEI(补充增强信息)单元的话,它必须在它所对应的基本编码图像的片段(slice)单元和数据划分片段(data partition)单元之前,并同时必须紧接在上一个基本编码图像的所有片段(slice)单元和数据划分片段(data partition)单元后边。假如SEI属于多个基本编码图像,其顺序仅以第一个基本编码图像为参照。
9.  如果存在图像分割符的话,它必须在所有SEI 单元、基本编码图像的所有片段slice)单元和数据划分片段(data partition)单元之前,并且紧接着上一个基本编码图像那些NAL单元。
10.  如果存在序列结束符,且序列结束符后还有图像,则该图像必须是IDR(即时解码器刷新)图像。序列结束符的位置应当在属于这个IDR图像的分割符、SEI 单元等数据之前,且紧接着前面那些图像的NAL单元。如果序列结束符后没有图像了,那么它的就在比特流中所有图像数据之后。
11.  流结束符在比特流中的最后。

h264有两种封装, 一种是Annexb模式,传统模式,有startcode,SPS和PPS是在ES中 一种是mp4模式,一般mp4 mkv会有,没有startcode,SPS和PPS以及其它信息被封装在container中,每一个frame前面是这个frame的长度 很多解码器只支持annexb这种模式,因此需要将mp4做转换 我们讨论的是第一种Annexb传统模式,

3.4 下面我们直接看代码,了解一下如何使用X264来编码h264文件

x264_param_default_preset():为了方便使用x264,只需要根据编码速度的要求和视频质量的要求选择模型,
并修改部分视频参数即可
x264_picture_alloc():为图像结构体x264_picture_t分配内存。
x264_encoder_open():打开编码器。
x264_encoder_encode():编码一帧图像。
x264_encoder_close():关闭编码器。
x264_picture_clean():释放x264_picture_alloc()申请的资源。
 
存储数据的结构体如下所示。
x264_picture_t:存储压缩编码前的像素数据。
x264_nal_t:存储压缩编码后的码流数据。

下面介绍几个重要的结构体
/********************************************************************************************
 x264_image_t 结构用于存放一帧图像实际像素数据。该结构体定义在x264.h中
*********************************************************************************************/
typedef struct
{
    int     i_csp;          // 设置彩色空间,通常取值 X264_CSP_I420,所有可能取值定义在x264.h中
    int     i_plane;        // 图像平面个数,例如彩色空间是YUV420格式的,此处取值3
    int     i_stride[4];    // 每个图像平面的跨度,也就是每一行数据的字节数
    uint8_t *plane[4];      // 每个图像平面存放数据的起始地址, plane[0]是Y平面,
                            // plane[1]和plane[2]分别代表U和V平面
}  x264_image_t;

/********************************************************************************************
x264_picture_t 结构体描述视频帧的特征,该结构体定义在x264.h中。
*********************************************************************************************/
typedef struct
{
int   i_type;           // 帧的类型,取值有X264_TYPE_KEYFRAME X264_TYPE_P
                        // X264_TYPE_AUTO等。初始化为auto,则在编码过程自行控制。
int   i_qpplus1;        // 此参数减1代表当前帧的量化参数值
int   i_pic_struct;     // 帧的结构类型,表示是帧还是场,是逐行还是隔行,
                        // 取值为枚举值 pic_struct_e,定义在x264.h中
int   b_keyframe;       // 输出:是否是关键帧
int64_t   i_pts;        // 一帧的显示时间戳
int64_t   i_dts;        // 输出:解码时间戳。当一帧的pts非常接近0时,该dts值可能为负。

/* 编码器参数设置,如果为NULL则表示继续使用前一帧的设置。某些参数
   (例如aspect ratio) 由于收到H264本身的限制,只能每隔一个GOP才能改变。
   这种情况下,如果想让这些改变的参数立即生效,则必须强制生成一个IDR帧。*/ 
x264_param_t    *param;

x264_image_t     img;    // 存放一帧图像的真实数据
x264_image_properties_t    prop;
x264_hrd_t    hrd_timing;// 输出:HRD时间信息,仅当i_nal_hrd设置了才有效
void    *opaque;         // 私有数据存放区,将输入数据拷贝到输出帧中
} x264_picture_t ;

/****************************************************************************************************************
x264_nal_t中的数据在下一次调用x264_encoder_encode之后就无效了,因此必须在调用
x264_encoder_encode 或 x264_encoder_headers 之前使用或拷贝其中的数据。
*****************************************************************************************************************/
typedef struct
{
int  i_ref_idc;        // Nal的优先级
int  i_type;           // Nal的类型
int  b_long_startcode; // 是否采用长前缀码0x00000001
int  i_first_mb;       // 如果Nal为一条带,则表示该条带第一个宏块的指数
int  i_last_mb;        // 如果Nal为一条带,则表示该条带最后一个宏块的指数
int  i_payload;        // payload 的字节大小
uint8_t *p_payload;    // 存放编码后的数据,已经封装成Nal单元
} x264_nal_t;


再来看看编码h264源码

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

FrameEncoder.cpp 源文件

//供测试文件使用,测试的时候打开
//#define ENCODE_OUT_FILE_1
//供测试文件使用
//#define ENCODE_OUT_FILE_2

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

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

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

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

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

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

    // set encoder parameters
    setParams();
    //按照色度空间分配内存,即为图像结构体x264_picture_t分配内存,并返回内存的首地址作为指针
    //i_csp(图像颜色空间参数,目前只支持I420/YUV420)为X264_CSP_I420
    x264_picture_alloc(&pic_in, params.i_csp, params.i_width, params.i_height);
    //create the encoder using our params 打开编码器
    encoder = x264_encoder_open(¶ms);

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

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

    return true;
}
//编码h264帧
int FrameEncoder::encodeFrame(char* inBytes, int frameSize, int pts,
                               char* outBytes, int *outFrameSize) {
    //YUV420P数据转化为h264
    int i420_y_size = in_width * in_height;
    int i420_u_size = (in_width >> 1) * (in_height >> 1);
    int i420_v_size = i420_u_size;

    uint8_t *i420_y_data = (uint8_t *)inBytes;
    uint8_t *i420_u_data = (uint8_t *)inBytes + i420_y_size;
    uint8_t *i420_v_data = (uint8_t *)inBytes + i420_y_size + i420_u_size;
    //将Y,U,V数据保存到pic_in.img的对应的分量中,还有一种方法是用AV_fillPicture和sws_scale来进行变换
    memcpy(pic_in.img.plane[0], i420_y_data, i420_y_size);
    memcpy(pic_in.img.plane[1], i420_u_data, i420_u_size);
    memcpy(pic_in.img.plane[2], i420_v_data, i420_v_size);

    // and encode and store into pic_out
    pic_in.i_pts = pts;
    //最主要的函数,x264编码,pic_in为x264输入,pic_out为x264输出
    int frame_size = x264_encoder_encode(encoder, &nals, &num_nals, &pic_in,
                                         &pic_out);

    if (frame_size) {
        /*Here first four bytes proceeding the nal unit indicates frame length*/
        int have_copy = 0;
        //编码后,h264数据保存为nal了,我们可以获取到nals[i].type的类型判断是sps还是pps
        //或者是否是关键帧,nals[i].i_payload表示数据长度,nals[i].p_payload表示存储的数据
        //编码后,我们按照nals[i].i_payload的长度来保存copy h264数据的,然后抛给java端用作
        //rtmp发送数据,outFrameSize是变长的,当有sps pps的时候大于1,其它时候值为1
        for (int i = 0; i < num_nals; i++) {
            outFrameSize[i] = nals[i].i_payload;
            memcpy(outBytes + have_copy, nals[i].p_payload, nals[i].i_payload);
            have_copy += nals[i].i_payload;
        }
#ifdef ENCODE_OUT_FILE_1
        fwrite(outBytes, 1, frame_size, out1);
#endif

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

        return num_nals;
    }
    return -1;
}

最后,我们来看看抛往java层的h264数据,在MediaEncoder.java中,函数startVideoEncode:

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

        videoEncoderThread = new Thread() {
            @Override
            public void run() {
                //视频消费者模型,不断从队列中取出视频流来进行h264编码
                while (videoEncoderLoop && !Thread.interrupted()) {
                    try {
                        //队列中取视频数据
                        VideoData videoData = videoQueue.take();
                        fps++;
                        byte[] outbuffer = new byte[videoData.width * videoData.height];
                        int[] buffLength = new int[10];
                        //对YUV420P进行h264编码,返回一个数据大小,里面是编码出来的h264数据
                        int numNals = StreamProcessManager.encoderVideoEncode(videoData.videoData, videoData.videoData.length, fps, outbuffer, buffLength);
                        //Log.e("RiemannLee", "data.length " +  videoData.videoData.length + " h264 encode length " + buffLength[0]);
                        if (numNals > 0) {
                            int[] segment = new int[numNals];
                            System.arraycopy(buffLength, 0, segment, 0, numNals);
                            int totalLength = 0;
                            for (int i = 0; i < segment.length; i++) {
                                totalLength += segment[i];
                            }
                            //Log.i("RiemannLee", "###############totalLength " + totalLength);
                            //编码后的h264数据
                            byte[] encodeData = new byte[totalLength];
                            System.arraycopy(outbuffer, 0, encodeData, 0, encodeData.length);
                            if (sMediaEncoderCallback != null) {
                                sMediaEncoderCallback.receiveEncoderVideoData(encodeData, encodeData.length, segment);
                            }
                            //我们可以把数据在java层保存到文件中,看看我们编码的h264数据是否能播放,h264裸数据可以在VLC播放器中播放
                            if (SAVE_FILE_FOR_TEST) {
                                videoFileManager.saveFileData(encodeData);
                            }
                        }
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                        break;
                    }
                }

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

此时,h264数据已经出来了,我们就实现了YUV420P的数据到H264数据的编码,接下来,我们再来看看音频数据。

3.5 android音频数据如何使用fdk-aac库来编码音频,转化为AAC数据的,直接上代码

public class AudioRecoderManager {

    private static final String TAG = "AudioRecoderManager";

    // 音频获取
    private final static int SOURCE = MediaRecorder.AudIOSource.MIC;

    // 设置音频采样率,44100是目前的标准,但是某些设备仍然支 2050 6000 1025
    private final static int SAMPLE_HZ = 44100;

    // 设置音频的录制的声道CHANNEL_IN_STEREO为双声道,CHANNEL_CONFIGURATION_MONO为单声道
    private final static int CHANNEL_CONFIG = AudioFormat.CHANNEL_IN_STEREO;

    // 音频数据格式:PCM 16位每个样本保证设备支持。PCM 8位每个样本 不一定能得到设备支持
    private final static int FORMAT = AudioFormat.ENCODING_PCM_16BIT;

    private int mBufferSize;

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

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

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

        public void startAudioIn() {

        workThread = new Thread() {
            @Override
            public void run() {
                mAudioRecord.startRecording();
                byte[] audioData = new byte[mBufferSize];
                int readsize = 0;
                //录音,获取PCM裸音频,这个音频数据文件很大,我们必须编码成AAC,这样才能rtmp传输
                while (loop && !Thread.interrupted()) {
                    try {
                        readsize += mAudioRecord.read(audioData, readsize, mBufferSize);
                        byte[] ralAudio = new byte[readsize];
                        //每次录音读取4K数据
                        System.arraycopy(audioData, 0, ralAudio, 0, readsize);
                        if (audioDataListener != null) {
                            //把录音的数据抛给MediaEncoder去编码AAC音频数据
                            audioDataListener.audioData(ralAudio);
                        }
                        //我们可以把裸音频以文件格式存起来,判断这个音频是否是好的,只需要加一个WAV头
                        //即形成WAV无损音频格式
                        if (SAVE_FILE_FOR_TEST) {
                            fileManager.saveFileData(ralAudio);
                        }

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

        loop = true;
        workThread.start();
    }

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

        if (SAVE_FILE_FOR_TEST) {
            fileManager.closeFile();
            //测试代码,以WAV格式保存数据啊
            PcmToWav.copyWaveFile(FileManager.TEST_PCM_FILE, FileManager.TEST_WAV_FILE, SAMPLE_HZ, bufferSizeInBytes);
        }
    }

我们再来看看MediaEncoder是如何编码PCM裸音频的

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

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

 

进入audio的jni编码

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

现在,我们进入了AudioEncoder,进入了音频编码的世界

AudioEncoder::AudioEncoder(int channels, int sampleRate, int bitRate)
{
    this->channels = channels;
    this->sampleRate = sampleRate;
    this->bitRate = bitRate;
}
............
/**
 * 初始化fdk-aac的参数,设置相关接口使得
 * @return
 */
int AudioEncoder::init() {
    //打开AAC音频编码引擎,创建AAC编码句柄
    if (aacEncOpen(&handle, 0, channels) != AACENC_OK) {
        LOGI("Unable to open fdkaac encodern");
        return -1;
    }
    // 下面都是利用aacEncoder_SetParam设置参数
    // AACENC_AOT设置为aac lc
    if (aacEncoder_SetParam(handle, AACENC_AOT, 2) != AACENC_OK) {
        LOGI("Unable to set the AOTn");
        return -1;
    }

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

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

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

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

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

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

    //返回数据给上层,表示每次传递多少个数据最佳,这样encode效率最高
    int inputSize = channels * 2 * info.frameLength;
    LOGI("inputSize = %d", inputSize);

    return inputSize;
}

我们终于知道MediaEncoder构造函数中初始化音频数据的用意了,它会返回设备中传递多少inputSize为最佳,这样,我们每次只需要传递相应的数据,就可以使得音频效率更优化

public void startAudioEncode() {
        if (audioEncoderLoop) {
            throw new RuntimeException("必须先停止");
        }
        audioEncoderThread = new Thread() {
            @Override
            public void run() {
                byte[] outbuffer = new byte[1024];
                int haveCopyLength = 0;
                byte[] inbuffer = new byte[audioEncodeBuffer];
                while (audioEncoderLoop && !Thread.interrupted()) {
                    try {
                        AudioData audio = audioQueue.take();
                        //我们通过fdk-aac接口获取到了audioEncodeBuffer的数据,即每次编码多少数据为最优
                        //这里我这边的手机每次都是返回的4096即4K的数据,其实为了简单点,我们每次可以让
                        //MIC录取4K大小的数据,然后把录取的数据传递到AudioEncoder.cpp中取编码
                        //Log.e("RiemannLee", " audio.audioData.length " + audio.audioData.length + " audioEncodeBuffer " + audioEncodeBuffer);
                        final int audioGetLength = audio.audioData.length;
                        if (haveCopyLength < audioEncodeBuffer) {
                            System.arraycopy(audio.audioData, 0, inbuffer, haveCopyLength, audioGetLength);
                            haveCopyLength += audioGetLength;
                            int remain = audioEncodeBuffer - haveCopyLength;
                            if (remain == 0) {
                                //fdk-aac编码PCM裸音频数据,返回可用长度的有效字段
                                int validLength = StreamProcessManager.encoderAudioEncode(inbuffer, audioEncodeBuffer, outbuffer, outbuffer.length);
                                //Log.e("lihuzi", " validLength " + validLength);
                                final int VALID_LENGTH = validLength;
                                if (VALID_LENGTH > 0) {
                                    byte[] encodeData = new byte[VALID_LENGTH];
                                    System.arraycopy(outbuffer, 0, encodeData, 0, VALID_LENGTH);
                                    if (sMediaEncoderCallback != null) {
                                        //编码后,把数据抛给rtmp去推流
                                        sMediaEncoderCallback.receiveEncoderAudioData(encodeData, VALID_LENGTH);
                                    }
                                    //我们可以把Fdk-aac编码后的数据保存到文件中,然后用播放器听一下,音频文件是否编码正确
                                    if (SAVE_FILE_FOR_TEST) {
                                        audioFileManager.saveFileData(encodeData);
                                    }
                                }
                                haveCopyLength = 0;
                            }
                        }
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                        break;
                    }
                }

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

我们看AudioEncoder是如何利用fdk-aac编码的

/**
 * Fdk-AAC库压缩裸音频PCM数据,转化为AAC,这里为什么用fdk-aac,这个库相比普通的aac库,压缩效率更高
 * @param inBytes
 * @param length
 * @param outBytes
 * @param outLength
 * @return
 */
int AudioEncoder::encodeAudio(unsigned char *inBytes, int length, unsigned char *outBytes, int outLength) {
    void *in_ptr, *out_ptr;
    AACENC_BufDesc in_buf = {0};
    int in_identifier = IN_AUDIO_DATA;
    int in_elem_size = 2;
    //传递input数据给in_buf
    in_ptr = inBytes;
    in_buf.bufs = &in_ptr;
    in_buf.numBufs = 1;
    in_buf.bufferIdentifiers = &in_identifier;
    in_buf.bufSizes = &length;
    in_buf.bufElSizes = &in_elem_size;

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

    AACENC_InArgs in_args = {0};
    in_args.numInSamples = length / 2;  //size为pcm字节数

    AACENC_OutArgs out_args = {0};
    AACENC_ERROR err;

    //利用aacEncEncode来编码PCM裸音频数据,上面的代码都是fdk-aac的流程步骤
    if ((err = aacEncEncode(handle, &in_buf, &out_buf, &in_args, &out_args)) != AACENC_OK) {
        LOGI("Encoding aac failedn");
        return err;
    }
    //返回编码后的有效字段长度
    return out_args.numOutBytes;
}

至此,我们终于把视频数据和音频数据编码成功了

视频数据:NV21==>YUV420P==>H264
音频数据:PCM裸音频==>AAC

四 . RTMP如何推送音视频流 最后我们看看rtmp是如何推流的:我们看看MediaPublisher这个类

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

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

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

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

        loop = true;
        rtmpThread.start();
    }
............
    private void onEncoderVideoData(byte[] encodeVideoData, int totalLength, int[] segment) {
        int spsLen = 0;
        int ppsLen = 0;
        byte[] sps = null;
        byte[] pps = null;
        int haveCopy = 0;
        //segment为C++传递上来的数组,当为SPS,PPS的时候,视频NALU数组大于1,其它时候等于1
        for (int i = 0; i < segment.length; i++) {
            int segmentLength = segment[i];
            byte[] segmentByte = new byte[segmentLength];
            System.arraycopy(encodeVideoData, haveCopy, segmentByte, 0, segmentLength);
            haveCopy += segmentLength;

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

向rtmp发送视频和音频数据的时候,实际上就是下面几个JNI函数

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

    /**
     * 发送SPS,PPS数据
     * @param sps       sps数据
     * @param spsLen    sps长度
     * @param pps       pps数据
     * @param ppsLen    pps长度
     * @param timeStamp 时间戳
     * @return
     */
    public static native int sendRtmpVideoSpsPPS(byte[] sps, int spsLen, byte[] pps, int ppsLen, long timeStamp);

    /**
     * 发送视频数据,再发送sps,pps之后
     * @param data
     * @param dataLen
     * @param timeStamp
     * @return
     */
    public static native int sendRtmpVideoData(byte[] data, int dataLen, long timeStamp);

    /**
     * 发送AAC Sequence HEAD 头数据
     * @param timeStamp
     * @return
     */
    public static native int sendRtmpAudioSpec(long timeStamp);

    /**
     * 发送AAC音频数据
     * @param data
     * @param dataLen
     * @param timeStamp
     * @return
     */
    public static native int sendRtmpAudioData(byte[] data, int dataLen, long timeStamp);

    /**
     * 释放RTMP连接
     * @return
     */
    public static native int releaseRtmp();

再来看看RtmpLivePublish是如何完成这几个jni函数的

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

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

    return 0;
}

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

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

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

//发送视频数据
JNIEXPORT jint JNICALL Java_com_riemannlee_liveproject_StreamProcessManager_sendRtmpVideoData
        (JNIEnv *env, jclass type, jbyteArray jvideoData, jint dataLen, jlong jstamp)
{
    if (rtmpLivePublish) {
        jbyte *video_data = env->GetByteArrayElements(jvideoData, NULL);

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

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

//发送音频Sequence头数据
JNIEXPORT jint JNICALL Java_com_riemannlee_liveproject_StreamProcessManager_sendRtmpAudioSpec
        (JNIEnv *env, jclass type, jlong jstamp)
{
    if (rtmpLivePublish) {
        rtmpLivePublish->addSequenceAacHeader(44100, 2, 0);
    }
    return 0;
}

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

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

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

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

最后再来看看RtmpLivePublish这个推流类是如何推送音视频的,rtmp的音视频流的推送有一个前提,需要首先发送

AVC sequence header 视频同步包的构造
AAC sequence header 音频同步包的构造

下面我们来看看AVC sequence的结构,AVC sequence header就是
AVCDecoderConfigurationRecord结构

 

这个协议对应于下面的代码:

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

    /*sps*/
    //3bit的reserved,二进制位111,5bit的numOfSequenceParameterSets,
    //sps个数,一般为1,及合起来二进制位11100001,即为0xe1
    body[i++]   = 0xe1;
    //SequenceParametersSetNALUnits(sps_size + sps)的数组
    body[i++] = (sps_len >> 8) & 0xff;
    body[i++] = sps_len & 0xff;
    memcpy(&body[i], sps, sps_len);
    i +=  sps_len;

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

对于AAC sequence header存放的是AudioSpecificConfig结构,该结构则在“ISO-14496-3 Audio”中描述。AudioSpecificConfig结构的描述非常复杂,这里我做一下简化,事先设定要将要编码的音频格式,其中,选择"AAC-LC"为音频编码,音频采样率为44100,于是AudioSpecificConfig简化为下表:

 

这个协议对应于下面的代码:

    //如上图所示
    //5bit audioObjectType 编码结构类型,AAC-LC为2 二进制位00010
    //4bit samplingFrequencyIndex 音频采样索引值,44100对应值是4,二进制位0100
    //4bit channelConfiguration 音频输出声道,对应的值是2,二进制位0010
    //1bit frameLengthFlag 标志位用于表明IMDCT窗口长度 0 二进制位0
    //1bit dependsOnCoreCoder 标志位,表面是否依赖与corecoder 0 二进制位0
    //1bit extensionFlag 选择了AAC-LC,这里必须是0 二进制位0
    //上面都合成二进制0001001000010000
    uint16_t audioConfig = 0 ;
    //这里的2表示对应的是AAC-LC 由于是5个bit,左移11位,变为16bit,2个字节
    //与上一个1111100000000000(0xF800),即只保留前5个bit
    audioConfig |= ((2 << 11) & 0xF800) ;

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

    //sampleRateIndex为4,二进制位0000001000000000 & 0000011110000000(0x0780)(只保留5bit后4位)
    audioConfig |= ((sampleRateIndex << 7) & 0x0780) ;
    //sampleRateIndex为4,二进制位000000000000000 & 0000000001111000(0x78)(只保留5+4后4位)
    audioConfig |= ((channel << 3) & 0x78) ;
    //最后三个bit都为0保留最后三位111(0x07)
    audioConfig |= (0 & 0x07) ;
    //最后得到合成后的数据0001001000010000,然后分别取这两个字节

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

至此,我们就分别构造了AVC sequence header 和AAC sequence header,这两个结构是推流的先决条件,没有这两个东西,解码器是无法解码的,最后我们再来看看我们把解码的音视频如何rtmp推送

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

    unsigned char *body = (unsigned char*)packet->m_body;
    //当NAL头信息中,type(5位)等于5,说明这是关键帧NAL单元
    //buf[0] NAL Header与运算,获取type,根据type判断关键帧和普通帧
    //00000101 & 00011111(0x1f) = 00000101
    int type = buf[0] & 0x1f;
    //Pframe  7:AVC
    body[0] = 0x27;
    //IDR I帧图像
    //Iframe  7:AVC
    if (type == NAL_SLICE_IDR) {
        body[0] = 0x17;
    }
    //AVCPacketType = 1
    /*nal unit,NALUs(AVCPacketType == 1)*/
    body[1] = 0x01;
    //composition time 0x000000 24bit
    body[2] = 0x00;
    body[3] = 0x00;
    body[4] = 0x00;

    //写入NALU信息,右移8位,一个字节的读取
    body[5] = (len >> 24) & 0xff;
    body[6] = (len >> 16) & 0xff;
    body[7] = (len >> 8) & 0xff;
    body[8] = (len) & 0xff;

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

    packet->m_hasAbsTimestamp = 0;
    packet->m_nBodySize = body_size;
    //当前packet的类型:Video
    packet->m_packetType = RTMP_PACKET_TYPE_VIDEO;
    packet->m_nChannel = 0x04;
    packet->m_headerType = RTMP_PACKET_SIZE_LARGE;
    packet->m_nInfoField2 = rtmp->m_stream_id;
    //记录了每一个tag相对于第一个tag(File Header)的相对时间
    packet->m_nTimeStamp = RTMP_GetTime() - start_time;

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

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

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

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

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

我们推送RTMP都是调用的libRtmp库的RTMP_SendPacket接口,先判断是否rtmp是通的,是的话推流即可,最后,我们看看rtmp是如何连接服务器的:

/**
 * 初始化RTMP数据,与rtmp连接
 * @param url
 */
void RtmpLivePublish::init(unsigned char * url) {
    this->rtmp_url = url;
    rtmp = RTMP_Alloc();
    RTMP_Init(rtmp);

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

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

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

至此,我们终于完成了rtmp推流的整个过程。

如果你对音视频开发感兴趣,觉得文章对您有帮助,别忘了点赞、收藏哦!或者对本文的一些阐述有自己的看法,有任何问题,欢迎在下方评论区与我讨论!

声明:本站部分内容来自互联网,如有版权侵犯或其他问题请与我们联系,我们将立即删除或处理。
▍相关推荐
更多资讯 >>>