NDK学习笔记:RtmpPusher之利用rtmpdump推h264/aac码流

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/a360940265a/article/details/88259475

NDK学习笔记:RtmpPusher之利用rtmpdump推h264/aac码流

本篇将是 RtmpPusher 的最后一篇。在之前的3篇文章里,我们已经把原生的视频YUV格式编码成h264,把音频的PCM格式编码成aac。现在我们借助rtmpdunmp库,把这些数据包以RTMP的协议打包并推到指定url的流服务器。这里需要配合生产者-消费者的设计模式来优雅地完成这个任务,整个落成框架如下图:

按照以上设计的流程框架,需要两个注意地方,第一点就是先进先出的队列,这次情况不能按照之前ffmpeg解码的时候利用内存池,一旦满了之后就停下来等待,因为获取的音视频流是不能被暂停的,所以这里只能使用无限制的容器双向队列。第二点就是确保写入队列的操作,和提取队列发送操作,两者同步并且是线程安全。

现在,我们把之前的 “未完待续” 都补全,先从视频h264码流的add_param_sequence(打包序列参数集)和add_common_frame(打包帧数据)开始,具体的改造代码如下:

// 打包h264的SPS与PPS->NALU->RTMPPacket队列
void add_param_sequence(unsigned char* pps, unsigned char* sps, int pps_len, int sps_len)
{
    int body_size = 16 + sps_len + pps_len; //按照H264标准配置SPS和PPS,共使用了16字节
    RTMPPacket *packet = malloc(sizeof(RTMPPacket));
    //RTMPPacket初始化
    RTMPPacket_Alloc(packet, body_size);
    RTMPPacket_Reset(packet);
    // 获取packet对象当中的m_body指针
    char * body = packet->m_body;
    int i = 0;
    //二进制表示:00010111
    body[i++] = 0x17;//VideoHeaderTag:FrameType(1=key frame)+CodecID(7=AVC)
    body[i++] = 0x00;//AVCPacketType=0(AVC sequence header)表示设置AVCDecoderConfigurationRecord
    //composition time 0x000000 24bit ?
    body[i++] = 0x00;
    body[i++] = 0x00;
    body[i++] = 0x00;
    /*AVCDecoderConfigurationRecord*/
    body[i++] = 0x01;//configurationVersion,版本为1
    body[i++] = sps[1];//AVCProfileIndication
    body[i++] = sps[2];//profile_compatibility
    body[i++] = sps[3];//AVCLevelIndication
    body[i++] = 0xFF;//lengthSizeMinusOne,H264视频中NALU的长度,计算方法是 1 + (lengthSizeMinusOne & 3),实际测试时发现总为FF,计算结果为4.
    /*sps*/
    body[i++] = 0xE1;//numOfSequenceParameterSets:SPS的个数,计算方法是 numOfSequenceParameterSets & 0x1F,实际测试时发现总为E1,计算结果为1.
    body[i++] = (unsigned char) ((sps_len >> 8) & 0xff);//sequenceParameterSetLength:SPS的长度
    body[i++] = (unsigned char) (sps_len & 0xff);//sequenceParameterSetNALUnits
    memcpy(&body[i], sps, (size_t) sps_len);
    i += sps_len;
    /*pps*/
    body[i++] = 0x01;//numOfPictureParameterSets:PPS 的个数,计算方法是 numOfPictureParameterSets & 0x1F,实际测试时发现总为E1,计算结果为1.
    body[i++] = (unsigned char) ((pps_len >> 8) & 0xff);//pictureParameterSetLength:PPS的长度
    body[i++] = (unsigned char) ((pps_len) & 0xff);//PPS
    memcpy(&body[i], pps, (size_t) pps_len);
    i += pps_len;

    //块消息头的类型(4种)
    packet->m_headerType = RTMP_PACKET_SIZE_MEDIUM;
    //消息类型ID(1-7协议控制;8,9音视频;10以后为AMF编码消息)
    packet->m_packetType = RTMP_PACKET_TYPE_VIDEO;
    //时间戳是绝对值还是相对值
    packet->m_hasAbsTimestamp = 0;
    //块流ID,Audio 和 Video通道
    packet->m_nChannel = 0x04;
    //记录了每一个tag相对于第一个tag(File Header)的相对时间。
    //以毫秒为单位。而File Header的time stamp永远为0。
    packet->m_nTimeStamp = 0;
    //消息流ID, last 4 bytes in a long header, 不在这里配置
    //packet->m_nInfoField2 = -1;
    //Payload Length
    packet->m_nBodySize = (uint32_t) body_size;
    //加入到RTMPPacket发送队列
    add_rtmp_packet(packet);
}
// 打包h264的图像(IPB)帧数据->NALU->RTMPPacket队列
void add_common_frame(unsigned char *buf ,int len)
{
    // 每个NALU之间通过startcode(起始码)进行分隔,起始码分成两种:0x000001(3Byte)或者0x00000001(4Byte)。
    // 如果NALU对应的Slice为一帧的开始就用0x00000001,否则就用0x000001。
    //去掉起始码(界定符)
    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 = malloc(sizeof(RTMPPacket));
    RTMPPacket_Alloc(packet, body_size);
    // 获取packet对象当中的m_body指针
    char * body = packet->m_body;
    //buf[0] NAL Header与运算,获取type,根据type判断关键帧和普通帧
    //当NAL头信息中,type(第一个字节的前5位)等于5,说明这是关键帧NAL单元
    int type = buf[0] & 0x1f;
    //Inter Frame 帧间压缩 普通帧
    body[0] = 0x27;//VideoHeaderTag:FrameType(2=Inter Frame)+CodecID(7=AVC)
    //IDR I帧图像
    if (type == NAL_SLICE_IDR) {
        body[0] = 0x17;//VideoHeaderTag:FrameType(1=key frame)+CodecID(7=AVC)
    }
    //AVCPacketType = 1
    body[1] = 0x01; /*nal unit,NALUs(AVCPacketType == 1)*/
    body[2] = 0x00; //composition time 0x000000 24bit
    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, (size_t) len);

    //块消息头的类型(4种)
    packet->m_headerType = RTMP_PACKET_SIZE_LARGE;
    //消息类型ID(1-7协议控制;8,9音视频;10以后为AMF编码消息)
    packet->m_packetType = RTMP_PACKET_TYPE_VIDEO;
    //时间戳是绝对值还是相对值
    packet->m_hasAbsTimestamp = 0;
    //块流ID,Audio和Vidio通道
    packet->m_nChannel = 0x04;
    //记录了每一个tag相对于第一个tag(File Header)的相对时间。
    packet->m_nTimeStamp = RTMP_GetTime() - gRtmpPusher->start_time;
    //消息流ID, last 4 bytes in a long header, 不在这里配置
    //packet->m_nInfoField2 = -1;
    //Payload Length
    packet->m_nBodySize = (uint32_t) body_size;
    //加入到RTMPPacket发送队列
    add_rtmp_packet(packet);
}

其中两个方法需要注意的地方就是,packet->m_nTimeStamp这个参数,因为参数集不是视频渲染内容的一部分,所以不影响这个编码的顺序,可以直接置为0。还要注意的地方就是,m_nInfoField2这个字段,这个是发送的时候用以区分消息流ID的,这个往下会讲到。RTMPPacket 按照 rtmpdump的demo流程使用,打包好数据包以后就可以把数据包加入到预备的队列当中。

接下就是改造音频的两个方法add_aac_body 和 add_aac_sequence_header,具体代码如下:

// 添加AAC编码的sequence header
void add_aac_sequence_header()
{
    if(gRtmpPusher==NULL || gRtmpPusher->faac_encoder==NULL)
        return;
    //从faacEncoder获取aac头信息
    unsigned char *spec_buf;
    unsigned long len; //长度
    faacEncGetDecoderSpecificInfo(gRtmpPusher->faac_encoder, &spec_buf, &len);
    uint32_t body_size = 2 + len;
    RTMPPacket *packet = malloc(sizeof(RTMPPacket));
    //RTMPPacket初始化
    RTMPPacket_Alloc(packet,body_size);
    RTMPPacket_Reset(packet);
    char * body = packet->m_body;
    //AUDIODATA的标志位,各位标志如下
    body[0] = 0xAF;
    // SoundFormat(4bits):10=AAC;
    // SoundRate(2bits):3=44kHz;
    // SoundSize(1bit):1=16-bit samples;
    // SoundType(1bit):1=Stereo sound;

    //AACAUDIODATA的AACPacketType
    body[1] = 0x00;
    // 1表示AAC raw,
    // 0表示AAC sequence header
    //AACAUDIODATA的RawData
    memcpy(&body[2], spec_buf, len); /*spec_buf是AAC sequence header数据*/

    packet->m_packetType = RTMP_PACKET_TYPE_AUDIO;
    packet->m_nBodySize = body_size;
    packet->m_nChannel = 0x04;
    packet->m_hasAbsTimestamp = 0;
    packet->m_nTimeStamp = 0;
    packet->m_headerType = RTMP_PACKET_SIZE_MEDIUM;
    add_rtmp_packet(packet);
    free(spec_buf);
}

// 添加AAC头->RTMPPacket队列
void add_aac_body(unsigned char *buf, int len)
{
    if(gRtmpPusher==NULL)
        return;
    int body_size = 2 + len;
    RTMPPacket *packet = malloc(sizeof(RTMPPacket));
    //RTMPPacket初始化
    RTMPPacket_Alloc(packet,body_size);
    RTMPPacket_Reset(packet);
    char * body = packet->m_body;
    //AUDIODATA的标志位,各位标志如下
    body[0] = 0xAF;
    // SoundFormat(4bits):10=AAC;
    // SoundRate(2bits):3=44kHz;
    // SoundSize(1bit):1=16-bit samples;
    // SoundType(1bit):1=Stereo sound;

    //AACAUDIODATA的AACPacketType
    body[1] = 0x01;
    // 1表示AAC raw,
    // 0表示AAC sequence header
    //AACAUDIODATA的RawData
    memcpy(&body[2], buf, (size_t) len); /*spec_buf是AAC raw数据*/

    packet->m_packetType = RTMP_PACKET_TYPE_AUDIO;
    packet->m_nBodySize = (uint32_t) body_size;
    packet->m_nChannel = 0x04;
    packet->m_hasAbsTimestamp = 0;
    packet->m_headerType = RTMP_PACKET_SIZE_LARGE;
    packet->m_nTimeStamp = RTMP_GetTime() - gRtmpPusher->start_time;
    add_rtmp_packet(packet);
}

同样道理,序列参数集是不参与解码的,所以packet->m_nTimeStamp=0;  然后又可以安安静静的加入到rtmp_packet队列中。

框架图的左半部分已经写好了,接下来就右半部分;先从启动推流native_startPush方法开始:

JNIEXPORT void JNICALL
Java_org_zzrblog_ffmp_RtmpPusher_startPush(JNIEnv *env, jobject jobj, jstring url_jstr)
{
    if(gRtmpPusher == NULL) {
        LOGE("%s","请先调用函数:prepareAudioEncoder&prepareVideoEncoder");
        return;
    }
    if(gRtmpPusher->rtmp_push_thread_id > 0) {
        LOGE("%s","rtmp_push_thread已启动,请stopPush");
        return;
    }
    // 初始化的操作
    const char* url_cstr = (*env)->GetStringUTFChars(env,url_jstr,NULL);
    // 复制url_cstr内容到rtmp_path
    gRtmpPusher->rtmp_path = malloc(strlen(url_cstr) + 1);
    memset(gRtmpPusher->rtmp_path, 0, strlen(url_cstr)+1);
    memcpy(gRtmpPusher->rtmp_path, url_cstr, strlen(url_cstr));
    //初始化互斥锁与条件变量
    pthread_mutex_init(&(gRtmpPusher->mutex), NULL);
    pthread_cond_init(&(gRtmpPusher->cond), NULL);
    //创建RTMPPacket双向队列
    create_queue();
    //启动消费者线程(从队列中不断拉取RTMPPacket发送给流媒体服务器)
    pthread_create(&(gRtmpPusher->rtmp_push_thread_id), NULL, rtmp_push_thread, NULL);
    gRtmpPusher->stop_rtmp_push_thread = 0;
    //RTMPPacket队列已经创建,发送AAC序列信息头
    add_aac_sequence_header();

    (*env)->ReleaseStringUTFChars(env,url_jstr,url_cstr);
    LOGI("%s","RtmpPusher startPush !");
}

我们把推送流媒体服务器的url缓存起来,然后创建同步需要的互斥锁和条件变量,以及RTMPPacket双向队列容器。接着就是启动消费线程开始不断的拉取RTMPPacket发送到指定的流媒体服务器。当所有准备流程启动以后,我们就可以主动的调用add_aac_sequence_header,发送AAC序列信息头。

关于自定义的RTMPPacket双向队列,就不带大家写了。有兴趣的同学可以到github->BlogApp工程下的 cpp/rtmppush/queue.c查看源码,这里提供头文件和思路。注意点就是前后节点前后指针的操作。

#ifndef BLOGAPP_QUEUE_H
#define BLOGAPP_QUEUE_H

typedef struct queue_node {
    struct queue_node* prev;
    struct queue_node* next;
    void *p; //节点的值
} QNode;

// 新建“双向链表”。成功,返回表头;否则,返回NULL
extern int create_queue();
// 撤销“双向链表”。成功,返回0;否则,返回-1
extern int destroy_queue();
// “双向链表是否为空”。为空的话返回1;否则,返回0。
extern int queue_is_empty();
// 返回“双向链表的大小”
extern int queue_size();
// 获取“双向链表中第1个元素”。成功,返回节点指针;否则,返回NULL。
extern void* queue_get_first();
// 获取“双向链表中最后1个元素”。成功,返回节点指针;否则,返回NULL。
extern void* queue_get_last();

// 将“value”插入到表头位置。成功,返回0;否则,返回-1。
extern int queue_insert_first(void *pval);
// 将“value”插入到末尾位置。成功,返回0;否则,返回-1。
extern int queue_append_last(void *pval);

// 删除组后一个节点。成功,返回0;否则,返回-1
extern int queue_delete_last();
// 删除第一个节点。成功,返回0;否则,返回-1
extern int queue_delete_first();

// 获取“双向链表中第index位置的元素”。成功,返回节点指针;否则,返回NULL。
extern void* queue_get(int index);
// 将“value”插入到index位置。成功,返回0;否则,返回-1。
extern int queue_insert(int index, void *pval);
// 删除“双向链表中index位置的节点”。成功,返回0;否则,返回-1
extern int queue_delete(int index);

#endif //BLOGAPP_QUEUE_H

之后,我们先完成 add_rtmp_packet 方法,此方法就是利用 queue_append_last 方法,每次追加数据包都是队列的尾部,这样就不会跃过之前填加的数据包了。  记得要加锁同步操作。

// 加入RTMPPacket双向队列,等待发送线程消费
void add_rtmp_packet(RTMPPacket *packet) {
    if(gRtmpPusher == NULL)
        return;

    pthread_mutex_lock(&(gRtmpPusher->mutex));
    if(gRtmpPusher->stop_rtmp_push_thread == 0) {
        queue_append_last(packet);
    }
    // 通知消费线程,有数据添加了。
    pthread_cond_signal(&(gRtmpPusher->cond));
    pthread_mutex_unlock(&(gRtmpPusher->mutex));
}

流程一步步的开始明朗起来了,最后剩下消费者线程,RTMPPacket的发送线程了。感觉这部分也没啥难点,毕竟那么多篇NDK学习笔记走来,这些都是小case了。

/**
 * Send RTMPPacket 工作线程
 * 从双向队列中不断提取RTMPPacket发送到指定的流媒体服务器
 */
void* rtmp_push_thread(void * arg) {
    //创建RTMP对象
    RTMP *rtmp = RTMP_Alloc();
    if(!rtmp){
        LOGE("rtmp初始化失败");
        goto end;
    }
    //初始化rtmp
    RTMP_Init(rtmp);
    //设置连接超时时间
    rtmp->Link.timeout = 5;
    //设置流媒体地址
    RTMP_SetupURL(rtmp, gRtmpPusher->rtmp_path);
    //发布rtmp数据流
    RTMP_EnableWrite(rtmp);
    //建立连接
    if(!RTMP_Connect(rtmp,NULL)){
        LOGE("%s","RTMP 连接失败");
        goto end;
    }
    // 初始化启动计时
    gRtmpPusher->start_time = RTMP_GetTime();
    if(!RTMP_ConnectStream(rtmp,0)) { //连接流
        goto end;
    }

    while(gRtmpPusher->stop_rtmp_push_thread == 0)
    {
        pthread_mutex_lock(&(gRtmpPusher->mutex));
        // 等待RTMPPacket加入到双向队列,然后 pthread_cond_signal
        pthread_cond_wait(&(gRtmpPusher->cond), &(gRtmpPusher->mutex));
        //取出队列中的RTMPPacket
        RTMPPacket *packet = queue_get_first();
        if(packet) {
            queue_delete_first(); //从队列中移除当前节点
            //注意:Note
            // 我们在打包RTMPPacket的时候,没有填 m_nInfoField2,
            // 是因为打包的时候不知道对应的stream_id是多少
            // 这里装填RTMPPacket的m_nInfoField2字段值 = RTMP的推送流ID stream_id
            packet->m_nInfoField2 = rtmp->m_stream_id;
            int i = RTMP_SendPacket(rtmp,packet,TRUE); //TRUE放入librtmp队列中,并不是立即发送
            if(!i){
                LOGE("RTMP 断开");
                RTMPPacket_Free(packet);
                pthread_mutex_unlock(&(gRtmpPusher->mutex));
                goto end;
            }
            RTMPPacket_Free(packet);
        }
        pthread_mutex_unlock(&(gRtmpPusher->mutex));
    }
end:
    LOGI("%s","释放资源");
    RTMP_Close(rtmp);
    RTMP_Free(rtmp);
    return 0;
}

从RTMPPacket双向队列获取数据包之后,之前在打包h264/aac成RTMPPacket没有填写的 m_nInfoField2字段就在这里补全了,就是RTMP对象的流ID字段。该字段是用以区分发送多路数据包的情况,这样就不会导致多路的时候出现数据错乱的问题了。

到此RTMPPusher的雏形就搞定了,需要一个流媒体服务器来验证其工作的有效性了。  借助nginx搭建流媒体服务器网上有很多例子,我就不做搬砖工作了,确实有需要的话,私信联系。

补充一下CMakeList编译脚本:

add_library(x264 STATIC IMPORTED )
set_target_properties(x264 PROPERTIES IMPORTED_LOCATION
            ${PROJECT_SOURCE_DIR}/src/main/cpp/rtmppush/x264/libx264.a)
set_target_properties(x264 PROPERTIES LINKER_LANGUAGE CXX)

add_library(rtmp STATIC IMPORTED )
set_target_properties(rtmp PROPERTIES IMPORTED_LOCATION
            ${PROJECT_SOURCE_DIR}/src/main/cpp/rtmppush/rtmp/librtmp.a)
set_target_properties(rtmp PROPERTIES LINKER_LANGUAGE CXX)

add_library(faac STATIC IMPORTED )
set_target_properties(faac PROPERTIES IMPORTED_LOCATION
            ${PROJECT_SOURCE_DIR}/src/main/cpp/rtmppush/faac/libfaac.a)
set_target_properties(faac PROPERTIES LINKER_LANGUAGE CXX)

add_library( # 生成动态库的名称
             rtmp-push
             # 指定是动态库
             SHARED
             # 编译库的源代码文件
             src/main/cpp/common/zzr_common.c
             src/main/cpp/rtmppush/queue.c
             src/main/cpp/rtmppush/rtmp_push.c )

target_link_libraries( # 指定目标链接库
                       rtmp-push
                       # 添加预编译库到目标链接库中
                       ${log-lib}
                       x264
                       faac
                       rtmp)

 

接下来开展新的系列内容,算是我擅长的内容,OpenGL.ES进阶版(NDK),(游戏)渲染引擎以及OpenGL.Shader的学习文章,大家敬请期待。

猜你喜欢

转载自blog.csdn.net/a360940265a/article/details/88259475