RTMP (三)音视频采集与数据封包

作者: zcwfeng | 来源:发表于2020-11-01 11:49 被阅读0次

    目录:

    RTMP(一)录屏直播理论入门
    RTMP(二)搭建推流服务
    RTMP (三)音视频采集与数据封包
    RTMP(四)交叉编译与CameraX
    RTMP (五)摄像头数据处理
    RTMP (六)音视频编码推流

    音频采集

    1. AudioRecord Android SDK 自带。直接在java端采集,相对方便,而且编码方式用的是MediaCodec,所以是首选

    2.OpenSL ES 效率高,但是底层C++层和java来回传递麻烦

    3.MediaRecord 但是我们无法干预采集过程

    AudioRecord 采集

    // 创建AudioRecord 录音
            // 最小缓冲区大小
            minBufferSize = AudioRecord.getMinBufferSize(
                    44100,
                    AudioFormat.CHANNEL_IN_MONO,
                    AudioFormat.ENCODING_PCM_16BIT);
            audioRecord = new AudioRecord(
                    MediaRecorder.AudioSource.MIC,
                    44100,
                    AudioFormat.CHANNEL_IN_DEFAULT,
                    AudioFormat.ENCODING_PCM_16BIT,
                    minBufferSize);
    

    音频数据

    RTMP的音频数据相对视频比较简单,只需要根据是否为音频audio specific config(记录音频的格式)。如果为 audio specific config拼接0xAF,0x00,否则就只需要添加0xAF,0x00。

    音频数据封包

    RTMP音频包数据&FLV 格式&0xAF的由来

    文件内容:Header,Version,Meta-Data (元数据 可以发也可以不发),音频数据,视频数据

    rtmp_flv_format.png

    RTMP 包中封装的音视频数据流,其实和FLV/tag封装音频和视频数据的方式是相同的,所以我们只需要按照FLV格式封装音视频即可。

    codec_flv.png

    我们的编码为,10:AAC,3:44100采样率,1:采样长度,1:声道。按照位数表示数据就为:0xAF

    封包格式二进制.png

    我们在Java端定义一个对应的RTMPPacckage 封包结构类

    public class RTMPPackage {
    
    
        public static final int RTMP_PACKET_TYPE_VIDEO = 0;
        public static final int RTMP_PACKET_TYPE_AUDIO_HEAD = 1;
        public static final int RTMP_PACKET_TYPE_AUDIO_DATA = 2;
    
        private byte[] buffer;//buf
        private int type;//类型
        private long tms;//时间戳
    
       ...
    }
    

    所以我们的封包, byte 数据 封包 + 音频数据 封包

     RTMPPacket *packet = malloc(sizeof(RTMPPacket));
        // 因为我们会在音频数据前拼两个字节,才符合flv/rtmp 的格式
        RTMPPacket_Alloc(packet,len + 2);
        //10101111  根据flv的数据结构拼接
        packet->m_body[0] = 0xAF;
        // 可以播放的数据0x01,不能播放0x00
        packet->m_body[1] = 0x01;
    

    获取播放音频之前,先发送 audio Special config .这个不是可以播放的数据
    0x12,0x08

    //RTMPPackage 传给c++
            RTMPPackage rtmpPackage = new RTMPPackage();
            byte[] audioSpec = {0x12,0x08};
            rtmpPackage.setBuffer(audioSpec);
            rtmpPackage.setType(RTMPPackage.RTMP_PACKET_TYPE_AUDIO_HEAD);
            rtmpPackage.setTms(0);
            screenLive.addPacket(rtmpPackage);
    

    音频的数据在编码后封包。RTMPPackage需要一个时间,这个时间一般是相对时间,先给出第一帧的时间,代码片段:

     // 获取编码后的数据
                index = mediaCodec.dequeueOutputBuffer(bufferInfo, 0);
                //每次取完了,在让编码器塞数据
                while (index >= 0 && isRecording) {
    
                    ByteBuffer outBuffer = mediaCodec.getOutputBuffer(index);
                    byte[] data = new byte[bufferInfo.size];
                    outBuffer.get(data);
    
                    // 第一帧时间
                    if (startTime == 0) {
                        startTime = bufferInfo.presentationTimeUs / 1000;//绝对是间
                    }
                    //TODO 送去推流,封装成RTMPackage 把data发送
                    rtmpPackage = new RTMPPackage();
                    rtmpPackage.setBuffer(data);
                    rtmpPackage.setType(RTMPPackage.RTMP_PACKET_TYPE_AUDIO_DATA);
                    //相对时间
                    rtmpPackage.setTms(bufferInfo.presentationTimeUs / 1000 - startTime);
                    screenLive.addPacket(rtmpPackage);
    
                    //释放输出队列,让其能存放新的数据
                    mediaCodec.releaseOutputBuffer(index, false);
                    index = mediaCodec.dequeueOutputBuffer(bufferInfo, 0);
                }
    

    RTMP视频数据 与 交叉编译

    • rtmp & rtmps

    rtmp librtmp库

    rtmps librtmp 库 + openssl 库编译

    用到了我之前文章介绍的RTMPDump:

    是一个用来处理RTMP流媒体的开源工具包。它能够单独使用进行 RTMP的通信,也可以集成到FFmpeg中通过FFmpeg接口来使用RTMPDump。

    交叉编译

    在Android中可以直接借助NDK在JNI层调用RTMPDump来完成RTMP通信。但是首先必须得进行交叉编译。 RTMPDump源码结构如下:

    librtmp.png

    在根目录下提供了一个 Makefile 与一些 .c 源文件。这里的源文件将会编译出一系列的可执行文件。然后我们需
    要的并不是可执行文件,真正的对RTMP的实现都在librtmp子目录中。在这个子目录中同样包含了一个 Makefile
    文件。通过阅读 Makefile 发现,它的源码并不多: OBJS=rtmp.o log.o amf.o hashswf.o parseurl.o 。因此我们 不进行预编译,即直接放入AS中借助 CMakeLists.txt 来进行编译。这么做可以让我们方便的对库本身进行调试或 修改(实际上我们确实会稍微修改这个库的源码)。

    在AS中复制librtmp置于src/main/cpp/librtmp并为其编写CMakeLists.txt

    #预编译宏
    set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -DNO_CRYPTO" ) 
    #所有源文件放入 rtmp_source 变量
    file(GLOB rtmp_source *.c)
    #编译静态库
    add_library(rtmp STATIC ${rtmp_source} )
    

    在 app/CMakeLists.txt 中导入这个CMakeLists.txt

    cmake_minimum_required(VERSION 3.10.2)
     # 导 入 其 他 目 录 cmakelist
     add_subdirectory(src/main/cpp/librtmp)
     add_library(XXX SHARED ...) 
    #XXX需要链接rtmp库
     target_link_libraries(XXX rtmp ...)
    

    RTMP视频流格式与FLV很相似,通过查看FLV的格式文档,就能够知道RTMP视频数据应该怎么拼接。

    视频采集封包

    RTMP视频包数据 & FLV 格式

    flv_tag.png

    FLV tags 结构

    字段 字节 描述
    类型 1 0x08: 音频0x09: 视频0x12: 脚本(描述信息)
    数据大小 3 数据区的大小,不包括包头。
    时间戳 3 当前帧相对时间戳,单位是毫秒。相对于第一个TAG时戳。
    时戳扩展 1 如果时戳大于0xFFFFFF,将会存在字节。
    流ID 3 总是0
    数据区 n 音、视频包
    视频fp.png 视频包格式.png

    如图,第一个字节0x09 表示此段数据为视频,数据大小为0x00,0x00,0x2F即47,时间戳为 0x00,0x00,0x00,时间戳扩展也为0x00。(第二行)流ID:0x00,0x00,0x00。接下来就是视频数据,
    通过此处的 数据大小字段得知,数据长为47字节。 则从0x17开始,一直到最后一行的0xC0,就是数据区域,而最后的
    0x00,0x00,0x00,0x3A 即58,表示的是这个数据块除最后4个字节的总大小。本处为视频数据,那么从0x17 开始,数据内容则为下面的部分。

    视频数据_1.png

    视频数据

    字段 占位 描述
    帧类型 4 1: 关键帧,2: 普通帧 ......
    编码ID 4 7: 高级视频编码 AVC...
    视频数据 n AVC则需要下面的AVCVIDEOPACKET

    AVCVIDEOPACKET

    字段 字节 描述
    类型 1 0:AVC 序列头(指导播放器如何解码) 1:其他单元(其他NALU)
    合成时间 3 对于AVC序列头,全为0
    数据 n 类型不同,数据不同
    视频数据2.png

    视频数据中 0x17则表示了1:关键帧与7:高级视频编码 AVC,如果是普通帧,则此数据为0x27。而类型为: 0x00表示这段数据为AVC序列头(avc sequence header)。最后三个字节为合成时间。而如果类型为AVC序列 头接下来的数据就是下面的内容:

    AVC 序列头

    在AVCVIDEOPACKET 中如果类型为0,则后续数据为:

    类型 字节 说明
    版本 1 0x01
    编码规格 3 sps[1]+sps[2]+sps[3] (后面说明)
    几个字节表示 NALU 的长度 1 0xFF,包长为 (0xFF& 3) + 1,也就是4字节表示
    SPS个数 1 0xE1,个数为0xE1 & 0x1F 也就是1
    SPS长度 2 整个sps的长度
    sps的内容 n 整个sps
    pps个数 1 0x01,不用计算就是1
    pps长度 2 整个pps长度
    pps内容 n 整个pps内容
    视频数据3.png

    0x01为版本,后续数据按照上表记录,最后四字节上面说过:为这个数据块除最后4个字节的总大小。其中 SPS与PPS是编码器在编码H.264视频时,在关键帧前会编码出的关于这个关键帧与需要参考该关键帧的B/P 帧如何解码的内容,如:宽、高等信息。

    在AVCVIDEOPACKET 中如果类型为1(非AVC 序列头),则后续数据为

    类型 字节 说明
    包长 由AVC 序列头中定义 后续长度
    数据 n H.264数据
    avc.png

    所以对于视频的数据封装,AVC序列头为:

    int i = 0;
    //AVC sequence header 与IDR一样
    packet->m_body[i++] = 0x17;
    //AVC sequence header 设置为0x00
    packet->m_body[i++] = 0x00;
    //CompositionTime
    packet->m_body[i++] = 0x00;
    packet->m_body[i++] = 0x00;
    packet->m_body[i++] = 0x00;
    //AVC sequence header
    packet->m_body[i++] = 0x01;//configurationVersion 版本号1
    packet->m_body[i++] = sps[1]; //profile 如baseline、main、 high
    packet->m_body[i++] = sp[2]; //profile_compatibility 兼容性
    packet->m_body[i++] = sp[3]; //profile level
    packet->m_body[i++] =0xFF; // reserved(111111) + lengthSizeMinusOne(2位 nal 长度) 总是0xFF;
    
    
    //sps
    packet->m_body[i++] = 0xE1; //reserved(111) + lengthSizeMinusOne(5位 sps 个数) 总是0xe1
    //sps length 2字节
    packet->m_body[i++] = (sps_len >> 8) & 0xff; //第0个字节 packet->m_body[i++] = sps_len & 0xff; //第1个字节 memcpy(&packet->m_body[i], sps,sps_len);
    i += sps_len;
    /*pps*/
    packet->m_body[i++] = 0x01; //pps number 
    //pps length
    packet->m_body[i++] = (pps_len >> 8) & 0xff; 
    packet->m_body[i++] = pps_len & 0xff;
     memcpy(&packet->m_body[i],pps, pps_len);
    

    而对于非AVC序列头,关键字与非关键字,只有第一个字节0x17与0x27的区别:

    packet->m_body[0] = 0x27;
    if (buf[0] == 0x65) { //关键帧 packet->m_body[0] = 0x17;
          LOGI("发送关键帧 data"); 
    }
    packet->m_body[1] = 0x01; 
    packet->m_body[2] = 0x00;
     packet->m_body[3] = 0x00; 
    packet->m_body[4] = 0x00;
    //长度
    packet->m_body[5] = (len >> 24) & 0xff; 
    packet->m_body[6] = (len >> 16) & 0xff;
    packet->m_body[7] = (len >> 8) & 0xff; 
    packet->m_body[8] = (len) & 0xff;
    
    //buf为编码出的一帧h.264数据 
    memcpy(&packet->m_body[9], buf, len);
    

    视频编码—H264

    视频编码格式,使用H.264编码算法(压缩算法)压缩视频数据。

    x264主页

    h264算法结构.png key_frame.png

    H.264数据

    H.264码流在网络中传输时实际是以NALU的形式进行传输的。NALU就是NAL UNIT,NAL单元。NAL全称Network Abstract Layer, 即网络抽象层。在H.264/AVC视频编码标准中,整个系统框架被分为了两个层面:视频编码层面 (VCL)和网络抽象层面(NAL)。其中,前者负责有效表示视频数据的内容,而后者则负责格式化数据并提供头 信息,以保证数据适合各种信道和存储介质上的传输。我们平时的每帧数据就是一个NAL单元。

    往RTMP包中填充的就是NAL数据,但不是直接将编码出来的数据填充进去。 一段包含了N个图像的H.264裸数据,每个NAL之间由:
    00 00 00 01或者 00 00 01
    进行分割。在分割符之后的第一个字节,就是表示这个nal的类型。
    0x67:sps
    0x68: pps
    0x65: IDR

    在将数据加入RTMPPacket的时候是需要去除分割符的。

    h264_video_1.png

    所以完整的封包代码为:

    buf += 4; //跳过 0x00 0x00 0x00 0x01
    len -= 4;
    int body_size = len + 9;
    RTMPPacket *packet = (RTMPPacket *) malloc(sizeof(RTMPPacket)); 
    RTMPPacket_Alloc(packet, len + 9);
    packet->m_body[0] = 0x27;
    if (buf[0] == 0x65) { //关键帧 
        packet->m_body[0] = 0x17;
        LOGI("发送关键帧 data"); 
    }
    packet->m_body[1] = 0x01; 
    packet->m_body[2] = 0x00; 
    packet->m_body[3] = 0x00; 
    packet->m_body[4] = 0x00;
    
    //长度
    packet->m_body[5] = (len >>24) & 0xff;
    packet->m_body[6] = (len >> 16) & 0xff;
    packet->m_body[7] = (len >> 8) & 0xff;
    packet->m_body[8] = (len) & 0xff;
    
    //数据
    memcpy(&packet->m_body[9], buf, len);
    

    NALU

    NALU就是NAL UNIT,nal单元。NAL全称Network Abstract Layer, 即网络抽象层,H.264在网络上传输的结构。一 帧图片经过 H.264编码器之后,就被编码为一个或多个片(slice),而装载着这些片(slice)的载体,就是 NALU 了 。

    相关文章

      网友评论

        本文标题:RTMP (三)音视频采集与数据封包

        本文链接:https://www.haomeiwen.com/subject/gykcvktx.html