rtmp推流:Android ffmpeg编码怎么实现rtmp推流

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

rtmp推流:Android ffmpeg编码怎么实现rtmp推流

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

一. 本文用到的库文件:

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

rtmp推流:Android ffmpeg编码怎么实现rtmp推流
rtmp推流:Android ffmpeg编码怎么实现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格式:

rtmp推流:Android ffmpeg编码怎么实现rtmp推流

YUV420P格式:

rtmp推流:Android ffmpeg编码怎么实现rtmp推流

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

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

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

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

再看一看具体实现

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

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

    return 0;
}

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

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

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

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

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

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

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

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

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

三 标准YUV420P数据编码为H264

多说无用,直接上代码

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

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

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

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

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

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

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

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

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

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

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

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

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

    return numNals;
}

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

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

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

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

SODB + RBSP STOP bit + 0bits = RBSP

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

NALU Header+EBSP=NALU(NAL单元)

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

NALU头结构

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

NALU类型结构图:

rtmp推流:Android ffmpeg编码怎么实现rtmp推流

其中,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的生成过程:

rtmp推流:Android ffmpeg编码怎么实现rtmp推流

即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文件来看看

rtmp推流:Android ffmpeg编码怎么实现rtmp推流
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
rtmp推流:Android ffmpeg编码怎么实现rtmp推流

对于这个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是竞争检测

rtmp推流:Android ffmpeg编码怎么实现rtmp推流

从前面我们分析知道,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单元的顺序是混乱的,必须将其重新依照规范组织后送入解码器,否则不能正确解码。

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结构

rtmp推流:Android ffmpeg编码怎么实现rtmp推流

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

    /*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简化为下表:

rtmp推流:Android ffmpeg编码怎么实现rtmp推流

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

    //如上图所示
    //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推流的整个过程。

版权声明:本文内容转自互联网,本文观点仅代表作者本人。本站仅提供信息存储空间服务,所有权归原作者所有。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至1393616908@qq.com 举报,一经查实,本站将立刻删除。

(1)

相关推荐

发表回复

登录后才能评论