范文健康探索娱乐情感热点
投稿投诉
热点动态
科技财经
情感日志
励志美文
娱乐时尚
游戏搞笑
探索旅游
历史星座
健康养生
美丽育儿
范文作文
教案论文

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

  要理解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)
  具体使用到哪些库中的接口我们将再下面进行细节展示。
  【相关学习资料推荐,点击下方链接免费报名 ,报名后会弹出学习资料免费 领取地址~】
  【免费】FFmpeg/WebRTC/RTMP/NDK/Android音视频流媒体高级开发-学习视频教程-腾讯课堂
  C++音视频更多学习资料 :点击莬费领取  音视频开发(资料文档+视频教程+面试题)(FFmpeg+WebRTC+RTMP+RTSP+HLS+RTP)
  二 . 如何从Camera摄像头获取视频流:
  2.1 Camera获取视频流,这个就不用多说了,只需要看到这个回调就行了,我们需要获取到这个数据:    //CameraSurfaceView.java中     @Override     public void onPreviewFrame(byte[] data, Camera camera) {         camera.addCallbackBuffer(data);         if (listener != null) {             listener.onCallback(data);         }     }    //阻塞线程安全队列,生产者和消费者     private LinkedBlockingQueue 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个字节
  【相关学习资料推荐,点击下方链接免费报名 ,报名后会弹出学习资料免费 领取地址~】
  【免费】FFmpeg/WebRTC/RTMP/NDK/Android音视频流媒体高级开发-学习视频教程-腾讯课堂
  下面我们来看看两个JNI函数,这个是摄像头转化的两个最关键的函数/**      * NV21转化为YUV420P数据      * @param src         原始数据      * @param width       原始数据宽度      * @param height      原始数据高度      * @param dst         生成数据      * @param dst_width   生成数据宽度      * @param dst_height  生成数据高度      * @param mode        模式      * @param degree      角度      * @param isMirror    是否镜像      * @return      */     public static native int compressYUV(byte[] src, int width, int height, byte[] dst, int dst_width, int dst_height, int mode, int degree, boolean isMirror);      /**      * YUV420P数据的裁剪      * @param src         原始数据      * @param width       原始数据宽度      * @param height      原始数据高度      * @param dst         生成数据      * @param dst_width   生成数据宽度      * @param dst_height  生成数据高度      * @param left        裁剪的起始x点      * @param top         裁剪的起始y点      * @return      */     public static native int cropYUV(byte[] src, int width, int height, byte[] dst, int dst_width, int dst_height, int left, int top);
  再看一看具体实现JNIEXPORT jint JNICALL Java_com_riemannlee_liveproject_StreamProcessManager_compressYUV     (JNIEnv *env, jclass type,      jbyteArray src_, jint width,      jint height, jbyteArray dst_,      jint dst_width, jint dst_height,      jint mode, jint degree,      jboolean isMirror) {      jbyte *Src_data = env->GetByteArrayElements(src_, NULL);     jbyte *Dst_data = env->GetByteArrayElements(dst_, NULL);     //nv21转化为i420(标准YUV420P数据) 这个temp_i420_data大小是和Src_data是一样的     nv21ToI420(Src_data, width, height, temp_i420_data);     //进行缩放的操作,这个缩放,会把数据压缩     scaleI420(temp_i420_data, width, height, temp_i420_data_scale, dst_width, dst_height, mode);     //如果是前置摄像头,进行镜像操作     if (isMirror) {         //进行旋转的操作         rotateI420(temp_i420_data_scale, dst_width, dst_height, temp_i420_data_rotate, degree);         //因为旋转的角度都是90和270,那后面的数据width和height是相反的         mirrorI420(temp_i420_data_rotate, dst_height, dst_width, Dst_data);     } else {         //进行旋转的操作         rotateI420(temp_i420_data_scale, dst_width, dst_height, Dst_data, degree);     }     env->ReleaseByteArrayElements(dst_, Dst_data, 0);     env->ReleaseByteArrayElements(src_, Src_data, 0);      return 0; }
  我们从java层传递过来的参数可以看到,原始数据是1080 * 1920,先转为1080 * 1920的标准的YUV420P的数据,下面的代码就是上面我举的例子,如何拆分YUV420P的Y,U,V分量和如何拆分YUV420SP的Y,UV分量,最后调用libyuv库的libyuv::NV21ToI420数据就完成了转换;然后进行缩放,调用了libyuv::I420Scale的函数完成转换//NV21转化为YUV420P数据 void nv21ToI420(jbyte *src_nv21_data, jint width, jint height, jbyte *src_i420_data) {     //Y通道数据大小     jint src_y_size = width * height;     //U通道数据大小     jint src_u_size = (width >> 1) * (height >> 1);      //NV21中Y通道数据     jbyte *src_nv21_y_data = src_nv21_data;     //由于是连续存储的Y通道数据后即为VU数据,它们的存储方式是交叉存储的     jbyte *src_nv21_vu_data = src_nv21_data + src_y_size;      //YUV420P中Y通道数据     jbyte *src_i420_y_data = src_i420_data;     //YUV420P中U通道数据     jbyte *src_i420_u_data = src_i420_data + src_y_size;     //YUV420P中V通道数据     jbyte *src_i420_v_data = src_i420_data + src_y_size + src_u_size;      //直接调用libyuv中接口,把NV21数据转化为YUV420P标准数据,此时,它们的存储大小是不变的     libyuv::NV21ToI420((const uint8 *) src_nv21_y_data, width,                        (const uint8 *) src_nv21_vu_data, width,                        (uint8 *) src_i420_y_data, width,                        (uint8 *) src_i420_u_data, width >> 1,                        (uint8 *) src_i420_v_data, width >> 1,                        width, height); }  //进行缩放操作,此时是把1080 * 1920的YUV420P的数据 ==> 480 * 640的YUV420P的数据 void scaleI420(jbyte *src_i420_data, jint width, jint height, jbyte *dst_i420_data, jint dst_width,                jint dst_height, jint mode) {     //Y数据大小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 videoQueue;     //音频流队列     private LinkedBlockingQueue 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 videoQueue;     //音频流队列     private LinkedBlockingQueue 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单元。
  【相关学习资料推荐,点击下方链接免费报名 ,报名后会弹出学习资料免费 领取地址~】
  【免费】FFmpeg/WebRTC/RTMP/NDK/Android音视频流媒体高级开发-学习视频教程-腾讯课堂
  C++音视频更多学习资料 :点击莬费领取  音视频开发(资料文档+视频教程+面试题)(FFmpeg+WebRTC+RTMP+RTSP+HLS+RTP)
  对应的代码定义如下    public static final int NAL_UNKNOWN     = 0;     public static final int NAL_SLICE       = 1; /* 非关键帧 */     public static final int NAL_SLICE_DPA   = 2;     public static final int NAL_SLICE_DPB   = 3;     public static final int NAL_SLICE_DPC   = 4;     public static final int NAL_SLICE_IDR   = 5; /* 关键帧 */     public static final int NAL_SEI         = 6;     public static final int NAL_SPS         = 7; /* SPS */     public static final int NAL_PPS         = 8; /* PPS */     public static final int NAL_AUD         = 9;     public static final int NAL_FILLER      = 12;
  由上面我们可以知道,h264字节流,就是由一些start code + NALU组成的,要组成一个NALU单元,首先要有原始数据,称之为SODB,它是原始的H264数据编码得到到,不包括3字节(0x000001)/4字节(0x00000001)的start code,也不会包括1字节的NALU头, NALU头部信息包括了一些基础信息,比如NALU类型。 ps:起始码包括两种,3字节0x000001和4字节0x00000001,在sps和pps和Access Unit的第一个NALU使用4字节起始码,其余情况均使用3字节起始码
  在 H264 SPEC 中,RBSP 定义如下: 在SODB结束处添加表示结束的bit 1来表示SODB已经结束,因此添加的bit 1成为rbsp_stop_one_bit,RBSP也需要字节对齐,为此需要在rbsp_stop_one_bit后添加若干0补齐,简单来说,要在SODB后面追加两样东西就形成了RBSP rbsp_stop_one_bit = 1 rbsp_alignment_zero_bit(s) = 0(s)
  RBSP的生成过程:
  即RBSP最后一个字节包含SODB最后几个比特,以及trailing bits其中,第一个比特位1,其余的比特位0,保证字节对齐,最后再结尾处添加0x0000,即CABAC_ZERO_WORD,从而形成 RBSP。
  EBSP的生成过程:NALU数据+起始码就形成了AnnexB格式(下面有介绍H264的两种格式,AnnexB为常用的格式),起始码包括两种,0x000001和0x00000001,为了不让NALU的主体和起始码之间产生竞争,在对RBSP进行扫描的时候,如果遇到连续两个0x00字节,则在该两个字节后面添加一个0x03字节,在解码的时候将该0x03字节去掉,也称为脱壳操作。解码器在解码时,首先逐个字节读取NAL的数据,统计NAL的长度,然后再开始解码。 替换规则如下: 0x000000 => 0x00000300 0x000001 => 0x00000301 0x000002 => 0x00000302 0x000003 => 0x00000303
  3.3 下面我们找一个h264文件来看看
  00 00 00 01 67 ... 这个为SPS,67为NALU Header,有type信息,后面即为我们说的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();     }
  【相关学习资料推荐,点击下方链接免费报名 ,报名后会弹出学习资料免费 领取地址~】
  【免费】FFmpeg/WebRTC/RTMP/NDK/Android音视频流媒体高级开发-学习视频教程-腾讯课堂
  C++音视频更多学习资料 :点击莬费领取  音视频开发(资料文档+视频教程+面试题)(FFmpeg+WebRTC+RTMP+RTSP+HLS+RTP)
  进入audio的jni编码//音频初始化 JNIEXPORT jint JNICALL Java_com_riemannlee_liveproject_StreamProcessManager_encoderAudioInit         (JNIEnv *env, jclass type, jint jsampleRate, jint jchannels, jint jbitRate) {     audioEncoder = new AudioEncoder(jchannels, jsampleRate, jbitRate);     int value = audioEncoder->init();     return value; }
  现在,我们进入了AudioEncoder,进入了音频编码的世界AudioEncoder::AudioEncoder(int channels, int sampleRate, int bitRate) {     this->channels = channels;     this->sampleRate = sampleRate;     this->bitRate = bitRate; } ............ /**  * 初始化fdk-aac的参数,设置相关接口使得  * @return  */ int AudioEncoder::init() {     //打开AAC音频编码引擎,创建AAC编码句柄     if (aacEncOpen(&handle, 0, channels) != AACENC_OK) {         LOGI("Unable to open fdkaac encoder ");         return -1;     }     // 下面都是利用aacEncoder_SetParam设置参数     // AACENC_AOT设置为aac lc     if (aacEncoder_SetParam(handle, AACENC_AOT, 2) != AACENC_OK) {         LOGI("Unable to set the AOT ");         return -1;     }      if (aacEncoder_SetParam(handle, AACENC_SAMPLERATE, sampleRate) != AACENC_OK) {         LOGI("Unable to set the sampleRate ");         return -1;     }      // AACENC_CHANNELMODE设置为双通道     if (aacEncoder_SetParam(handle, AACENC_CHANNELMODE, MODE_2) != AACENC_OK) {         LOGI("Unable to set the channel mode ");         return -1;     }      if (aacEncoder_SetParam(handle, AACENC_CHANNELORDER, 1) != AACENC_OK) {         LOGI("Unable to set the wav channel order ");         return 1;     }     if (aacEncoder_SetParam(handle, AACENC_BITRATE, bitRate) != AACENC_OK) {         LOGI("Unable to set the bitrate ");         return -1;     }     if (aacEncoder_SetParam(handle, AACENC_TRANSMUX, 2) != AACENC_OK) { //0-raw 2-adts         LOGI("Unable to set the ADTS transmux ");         return -1;     }      if (aacEncoder_SetParam(handle, AACENC_AFTERBURNER, 1) != AACENC_OK) {         LOGI("Unable to set the ADTS AFTERBURNER ");         return -1;     }      if (aacEncEncode(handle, NULL, NULL, NULL, NULL) != AACENC_OK) {         LOGI("Unable to initialize the encoder ");         return -1;     }      AACENC_InfoStruct info = { 0 };     if (aacEncInfo(handle, &info) != AACENC_OK) {         LOGI("Unable to get the encoder info ");         return -1;     }      //返回数据给上层,表示每次传递多少个数据最佳,这样encode效率最高     int inputSize = channels * 2 * info.frameLength;     LOGI("inputSize = %d", inputSize);      return inputSize; }
  我们终于知道MediaEncoder构造函数中初始化音频数据的用意了,它会返回设备中传递多少inputSize为最佳,这样,我们每次只需要传递相应的数据,就可以使得音频效率更优化public void startAudioEncode() {         if (audioEncoderLoop) {             throw new RuntimeException("必须先停止");         }         audioEncoderThread = new Thread() {             @Override             public void run() {                 byte[] outbuffer = new byte[1024];                 int haveCopyLength = 0;                 byte[] inbuffer = new byte[audioEncodeBuffer];                 while (audioEncoderLoop && !Thread.interrupted()) {                     try {                         AudioData audio = audioQueue.take();                         //我们通过fdk-aac接口获取到了audioEncodeBuffer的数据,即每次编码多少数据为最优                         //这里我这边的手机每次都是返回的4096即4K的数据,其实为了简单点,我们每次可以让                         //MIC录取4K大小的数据,然后把录取的数据传递到AudioEncoder.cpp中取编码                         //Log.e("RiemannLee", " audio.audioData.length " + audio.audioData.length + " audioEncodeBuffer " + audioEncodeBuffer);                         final int audioGetLength = audio.audioData.length;                         if (haveCopyLength < audioEncodeBuffer) {                             System.arraycopy(audio.audioData, 0, inbuffer, haveCopyLength, audioGetLength);                             haveCopyLength += audioGetLength;                             int remain = audioEncodeBuffer - haveCopyLength;                             if (remain == 0) {                                 //fdk-aac编码PCM裸音频数据,返回可用长度的有效字段                                 int validLength = StreamProcessManager.encoderAudioEncode(inbuffer, audioEncodeBuffer, outbuffer, outbuffer.length);                                 //Log.e("lihuzi", " validLength " + validLength);                                 final int VALID_LENGTH = validLength;                                 if (VALID_LENGTH > 0) {                                     byte[] encodeData = new byte[VALID_LENGTH];                                     System.arraycopy(outbuffer, 0, encodeData, 0, VALID_LENGTH);                                     if (sMediaEncoderCallback != null) {                                         //编码后,把数据抛给rtmp去推流                                         sMediaEncoderCallback.receiveEncoderAudioData(encodeData, VALID_LENGTH);                                     }                                     //我们可以把Fdk-aac编码后的数据保存到文件中,然后用播放器听一下,音频文件是否编码正确                                     if (SAVE_FILE_FOR_TEST) {                                         audioFileManager.saveFileData(encodeData);                                     }                                 }                                 haveCopyLength = 0;                             }                         }                     } catch (InterruptedException e) {                         e.printStackTrace();                         break;                     }                 }              }         };         audioEncoderLoop = true;         audioEncoderThread.start();     }
  我们看AudioEncoder是如何利用fdk-aac编码的/**  * Fdk-AAC库压缩裸音频PCM数据,转化为AAC,这里为什么用fdk-aac,这个库相比普通的aac库,压缩效率更高  * @param inBytes  * @param length  * @param outBytes  * @param outLength  * @return  */ int AudioEncoder::encodeAudio(unsigned char *inBytes, int length, unsigned char *outBytes, int outLength) {     void *in_ptr, *out_ptr;     AACENC_BufDesc in_buf = {0};     int in_identifier = IN_AUDIO_DATA;     int in_elem_size = 2;     //传递input数据给in_buf     in_ptr = inBytes;     in_buf.bufs = &in_ptr;     in_buf.numBufs = 1;     in_buf.bufferIdentifiers = &in_identifier;     in_buf.bufSizes = &length;     in_buf.bufElSizes = &in_elem_size;      AACENC_BufDesc out_buf = {0};     int out_identifier = OUT_BITSTREAM_DATA;     int elSize = 1;     //out数据放到out_buf中     out_ptr = outBytes;     out_buf.bufs = &out_ptr;     out_buf.numBufs = 1;     out_buf.bufferIdentifiers = &out_identifier;     out_buf.bufSizes = &outLength;     out_buf.bufElSizes = &elSize;      AACENC_InArgs in_args = {0};     in_args.numInSamples = length / 2;  //size为pcm字节数      AACENC_OutArgs out_args = {0};     AACENC_ERROR err;      //利用aacEncEncode来编码PCM裸音频数据,上面的代码都是fdk-aac的流程步骤     if ((err = aacEncEncode(handle, &in_buf, &out_buf, &in_args, &out_args)) != AACENC_OK) {         LOGI("Encoding aac failed ");         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推流的整个过程。
  如果你对音视频开发感兴趣,觉得文章对您有帮助,别忘了点赞、收藏哦!或者对本文的一些阐述有自己的看法,有任何问题,欢迎在下方评论区与我讨论!

土耳其800年穿越游当中世纪档案变成多感官数字体验澎湃新闻记者朱喆综合报道近日,在伊斯坦布尔最大的文化艺术活动贝伊奥卢文化节(BeyoluCultureRouteFestival)上,媒体艺术家雷菲克安纳多尔(RefikAnado冠心病人能否喝酒?日常生活要注意什么冠心病能喝酒吗?首先冠心病一定要保持一个良好的生活习惯,首先就要从生活起居开始入手,患者一定要戒烟戒酒,还要避免吃高脂肪以及高胆固醇的食物,患者要吃一些新鲜的水果和蔬菜,不但能够补101岁女国医脸色红润,头发浓密!她分享了5个日常习惯导语在上海有这样一位老人,百岁高龄仍坚持每周出诊,行医八十载,接诊患者逾百万人次,她就是国医大师中国中医科学院首批学部委员上海中医药大学终身教授朱南孙。她有着精湛的医术无私奉献的精身体内淤堵严重?两味中药泡水,消积化滞祛湿化痰打通淤堵体内有淤还有堵,身体淤堵严重怎么办?今天刘医生就来教你用两味中药,消积化滞,打通淤堵。有句老话叫,世上本无病,只是淤和堵,现如今的社会,很多人生病,大部分都和体内淤堵有关系,如果你脑梗是吃出来的?劝告3种食物易堵塞血管,再爱吃也管住嘴刘阿姨今年63,因为前些年被检查出肾结石,每年定期都有去复诊。前些日子,刘阿姨来到医院复诊,医生发现她鼻子两侧的沟纹不太对称,刘阿姨表示自己平日都没有注意到。医生询问了她最近的身体喝醋,真的能通血管降血压吗?网友提问我有点胖,还查出了三高。我老婆说醋能通血管降血压,天天逼着我喝醋,可难喝了!我想问问,这真的管用吗?醋是一味历史悠久的中药。约两千年前,醋就被用来治病了,最早可追溯至东汉张一碗血府逐瘀汤,通肝郁血瘀,消除患者5年功能障碍心脏疼血府逐瘀汤,出自医林改错方组桃仁红花当归生地黄牛膝川芎桔梗赤芍枳壳甘草柴胡。方组歌诀血府当归生地桃,红花枳壳膝芎饶。柴胡赤芍甘桔梗,血化下行不作痨。通窍全凭好麝香,桃红大枣老葱姜。自带黄体酮的5种食物,雌激素丰富,女人要多吃,别舍不得黄体是女性卵巢中,由许多黄色颗粒状细胞形成的内分泌腺体,出现在卵巢每次排卵后。黄体所分泌的激素主要是孕激素,可以达到抑制子宫收缩,使子宫黏膜增厚,促进乳腺分泌等作用。黄体酮是由卵巢健康小知识中医提示夏季饮食养生怎样做?夏季,古人又称为夏三月,是指从立夏之日起,到立秋前一日止,包括了立夏小满芒种夏至小暑大暑这六个节气。中医提示夏季饮食养生要注意以下提示1。夏季饮食少辛热夏季炎热,汗出溱溱,内热较重刘国梁迎喜讯!国乒19岁小将向鹏爆冷淘汰张本智和,对手气得跺脚近日在海外乒乓球比赛中,中国年轻小将的发挥非常亮眼,尤其是向鹏这位球星,他在对阵张本智和这位日本名将的时候,没有丝毫的懈怠,虽然两人年龄相仿,但是参加国际比赛的机会和境遇大不相同,首钢悍将正式离队,秦晓雯不讲情面,转投李楠强势冲击季后赛CBA首钢悍将正式离队,秦晓雯痛定思痛,有望辅佐李楠冲击季后赛。在这个休赛期动静最大的球队无疑就是北京首钢队,他们先是提拔解立彬成为球队的主教练,然后让王骁辉出任二队主帅,老将常林
慢品人间烟火闲时与你立黄昏,灶前笑问粥可温。就是因为这句话,迷恋上了浮生六记,没有波澜壮阔。豪情万丈,我爱的只是芸娘与三白氤氲在字里行间的人间烟火气。有时,我们会突然特别怀念一段感情一个人,只人间多惆怅,此冬尤其寒文夏风我们无法完全对世界袒露自己,但那些没说出口的部分,才使我们完整。那些没有目的的出发,才是最好的行程林婉瑜既然人间不能圆满,不如释然这个冬天分外寒冷,天空黯然,寒意涌动,时间也赵建不要让民企成为一种稀缺资源现在有个现象值得我们警惕,那就是民营企业正变得越来越稀缺。尤其是好的民营企业,具有国际竞争力的跨国民营企业,已经成为当前中国经济比较稀缺的资源。最近半年对提振民营企业发展信心促进民请问孟晓苏董事长,你会拿出13存款来买房吗?财经新势力新春季针对自己劝中国居民将家庭存款的13拿出来购房,用以恢复经济的观点引发舆论广泛关注和激烈争论,11日上午,孟晓苏对此进行了回应。他表示,2022年前11个月,中国商品V观财报奥联电子总经理续聘不足三月辞职,啥情况?中新经纬1月13日电奥联电子总经理傅宗朝辞职了,这距他第二次获聘总经理还不足3个月。1月12日晚间,奥联电子公告,于近日收到公司董事总经理傅宗朝的书面辞职报告。傅宗朝因个人原因,申这家股份行与房企客户打官司一年终和解涉房不良贷款曾暴增6倍四年前,浙商银行和房企北京国瑞签订贷款合同,约定向后者发放二十多亿元贷款。但随着房地产行业遭遇雷潮,北京国瑞后续违约,未能按时还本付息。大约一年前,为了追回这笔巨额贷款,浙商银行将100位富豪在瑞士存7。8万亿存款?为何半年内同一谣言流行了两次?5个月前的2022年8月,一则100名中国富豪在瑞士银行拥有7。8万亿元的消息轰动了全网,当时我写了一篇文章澄清了这件事情。没想到最近又传出了类似的消息,而且更加夸张。本次谣言不仅农行贵州省分行为乡村振兴注入金融力量来源人民网人民日报null作为服务三农的生力军,中国农业银行贵州省分行(简称农行贵州省分行)始终如一坚守在乡村振兴发展最前线,不断调整信贷结构创新产品模式优化业务流程,为多彩贵州现铝型材领先企业,鑫铂股份光伏及汽车轻量化驱动成长(报告出品方分析师东亚前海证券郑倩怡)1。聚焦铝型材领域,公司业绩大幅增长1。1。铝型材行业龙头,大力发展光伏新能源铝型材业务公司是国内铝型材龙头企业之一。公司主要从事工业铝型材工微观这一年房企布局大湾区多城,2022年却鲜少拿地,靠开发顶豪走红的鹏瑞集团能否持续发力?每经记者陈荣浩每经编辑魏文艺2022年在房企销售业绩普遍下滑的背景下,也有部分房企逆势发力,彰显黑马本色。从近期克而瑞公布的2022年大湾区各城房企销售情况看,来自深圳的房企深圳鹏东海岛三大巨无霸去年进出口逾790亿元湛江海关关员在船边监管出口钢卷。刘文雄摄南方日报讯(记者刘稳通讯员赖沛刘文雄周奕奂)1月7日,在湛江东海岛口岸政务服务中心,湛江海关所属东海岛海关关员正在为宝钢湛江钢铁有限公司办理