rtmp H264多Slice封装学习笔记

Posted vonchenchen1

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了rtmp H264多Slice封装学习笔记相关的知识,希望对你有一定的参考价值。

1.背景

在使用src_librtmp转推H264数据时,拉流端观看出现了花屏问题。经过排查发现客户端X264编码时如果设置了分片,转推为rtmp就会导致花屏,关闭分片相关设置视频正常。在转推H264前将数据写入本地,播放正常,播放转推后的rtmp花屏,ffplay会报错。这里推断是rtmp封装问题导致了花屏,下面首先需要对于这种一帧H264视频中包含多个Slice的情况,应当如何封装。

2.与ffmpeg推流进行对比

这里们尝试使用ffmpeg来推一段多slice的H264码流,ffplay拉流播放,很幸运一切正常。这样就可以抓包对比两种封装方式的差别,找出问题。下面先来分析一下ffmpeg的正确推流抓包。使用下面的命令将本地测试h264文件推送到本机的测试rtmp服务器上,这个h264文件每帧都由4个slice组成。

ffmpeg -re -i test.h264 -c copy -f flv rtmp://127.0.0.1:1935/live/dechen

下图是过滤得到的抓包数据,这里是推流的第一帧。不知道是rtmp格式问题还是Wireshark版本问题,似乎这里格式解析有点问题,下图红色字体是手动解析的结果,可以看到这里是一个H264关键帧,开头是AVCC格式封装的SPS,头4位为大端序列的长度信息,长度为0x17(十进制23),从0x67开始,到0x95正好23个字节,紧接着是AVCC格式封装的PPS,长度4个字节。再之后0x65就是一个H264视频的关键帧的开始,这里注意一下,这个NAL的长度为0x095f。现在我们开始找下一个NAL,这个NAL与第一个NAL同属于一帧。ffmpeg似乎并没有把SPS、PPS封装为extra data单独发送出去。

下图是下一个NAL所处的位置。我们与原始H264文件对照一下,看看同一帧的两个NAL中间有没有增加其他信息。

原始H264文件,对于红线附近的数据,除了将AnnexB的起始头换成了大端排列的帧长度,其他数据并没有变化。

下面照着这个方法查看当前Message中剩下的数据,可以发现总共正好有4个NAL。可以发现ffmpeg推流中每个完整的H264视频帧需要转换为AVCC格式,并且封装到同一个message中出去。

对比我们的抓包,可以发现我们每个NAL都会作为一个Message发送,下面看一下我们调用srs_librtmp的流程。这里的H264数据是从Webrtc的VideoReceiveStream中,视频解码前获取的EncodedFrame,如果接收到关键帧,会抛出一段AnnexB格式的 PSP+PPS+完整NAL的IDR 形式的数据,非关键帧也会收到一个完整NAL的帧,之后会将这些数据放入srs的 srs_h264_write_raw_frames 方法打包发送出去。下面看一下srs_h264_write_raw_frames的工作流程。代码如下,这里会解析传入的h264码流,将每次解析的结果通过 srs_write_h264_raw_frames 打包发送。

/**
 * write h264 multiple frames, in annexb format.
 */
int srs_h264_write_raw_frames(srs_rtmp_t rtmp, char* frames, int frames_size, char *sei, int sei_size, uint32_t dts, uint32_t pts)

    ...
    
    // send each frame.
    while (!stream->empty()) 
        char* frame = NULL;
        int frame_size = 0;
        //解析一个annexb数据,数据返回给frame,数据长度为frame_size
        if ((err = context->avc_raw.annexb_demux(stream, &frame, &frame_size)) != srs_success) 
            ret = srs_error_code(err);
            srs_freep(err);
            return ret;
        
        
        // ignore invalid frame,
        // atleast 1bytes for SPS to decode the type
        if (frame_size <= 0) 
            continue;
        
        
        //将解析得到的数据打包发送
        // it may be return error, but we must process all packets.
        if ((ret = srs_write_h264_raw_frame(context, frame, frame_size, sei, sei_size, dts, pts)) != ERROR_SUCCESS) 
            error_code_return = ret;
            
            // ignore known error, process all packets.
            if (srs_h264_is_dvbsp_error(ret)
                || srs_h264_is_duplicated_sps_error(ret)
                || srs_h264_is_duplicated_pps_error(ret)
                ) 
                continue;
            
            
            return ret;
        
    
    
    return error_code_return;

annexb_demux会把每个由 00 00 00 01这种的头分割的数据找出,对于分slice的H264数据,每个slice也会被分为一个一个的NAL,这样就导致了直接使用srs_write_h264_raw_frames写入后每个slice被分为一个message被分别发出。

srs_error_t SrsRawH264Stream::annexb_demux(SrsBuffer* stream, char** pframe, int* pnb_frame)

    srs_error_t err = srs_success;
    
    *pframe = NULL;
    *pnb_frame = 0;
    
    while (!stream->empty()) 
        // each frame must prefixed by annexb format.
        // about annexb, @see ISO_IEC_14496-10-AVC-2003.pdf, page 211.
        int pnb_start_code = 0;
        if (!srs_avc_startswith_annexb(stream, &pnb_start_code)) 
            return srs_error_new(ERROR_H264_API_NO_PREFIXED, "annexb start code");
        
        int start = stream->pos() + pnb_start_code;
        
        // find the last frame prefixed by annexb format.
        stream->skip(pnb_start_code);
        while (!stream->empty()) 
            if (srs_avc_startswith_annexb(stream, NULL)) 
                break;
            
            stream->skip(1);
        
        
        // demux the frame.
        *pnb_frame = stream->pos() - start;
        *pframe = stream->data() + start;
        break;
    
    
    return err;

这样就找到了我们打包方式与ffmpeg的不同,下面就需要尝试重新组合这些NAL,作为一个message发送出去。这里增加两个改写的自定义的方法,尝试封装多slice的H264为rtmp。使用 h264_write_raw_single_frame测试推流,ffplay就可以正常播放了。

/**
 *读取当前stream中剩余h264数据  全部封装为AVCC模式的h264 之后封装flv头
 *返回flv数据及其长度
 */
int h264_mux_single_frame_to_flv(srs_rtmp_t rtmp, SrsBuffer* stream, char **flv, int *nb_flv, char *sei, int sei_size, uint32_t dts, uint32_t pts)
    
    int ret = ERROR_SUCCESS;
    srs_error_t err = srs_success;
    
    char* frame = nullptr;
    int frame_size = 0;
    Context* context = (Context*)rtmp;
    
    // for IDR frame, the frame is keyframe.
    SrsVideoAvcFrameType frame_type = SrsVideoAvcFrameTypeInterFrame;
        
    //当前帧 封装为AVCC
    vector<string> avcc_nal_vec;
    int video_data_size = 0;
    while(!stream->empty())
        if ((err = context->avc_raw.annexb_demux(stream, &frame, &frame_size)) != srs_success) 
            ret = srs_error_code(err);
            srs_freep(err);
            return ret;
        
        
        SrsAvcNaluType nut = (SrsAvcNaluType)(frame[0] & 0x1f);
        
        //PrintCharArrayAsHex(frame, frame_size);
        if (nut != SrsAvcNaluTypeIDR && nut != SrsAvcNaluTypeNonIDR) 
            return ret;
        
        
        if (nut == SrsAvcNaluTypeIDR) 
            frame_type = SrsVideoAvcFrameTypeKeyFrame;
        

        std::string ibp;
        if ((err = context->avc_raw.mux_ipb_frame(frame, frame_size, ibp)) != srs_success) 
            ret = srs_error_code(err);
            srs_freep(err);
            return ret;
        
        
        video_data_size += ibp.size();
        avcc_nal_vec.push_back(ibp);
    
    
    //extradata size + avcc frame size
    //int totle_avcc_buf_size = sh.size() + video_data_size;
    int totle_avcc_buf_size = video_data_size;
    char* video_data_packet = new char[totle_avcc_buf_size];
    SrsAutoFreeA(char, video_data_packet);
    //当前帧AVCC合并
    memset(video_data_packet, 0, totle_avcc_buf_size);
    //拷贝extra data
    //memcpy(video_data_packet, sh.data(), sh.size());
    //拷贝AVCC视频
    //int index = sh.size();
    int index = 0;
    for(int i=0; i<avcc_nal_vec.size(); i++)
        memcpy(video_data_packet+index, avcc_nal_vec[i].data(), avcc_nal_vec[i].size());
        index += avcc_nal_vec[i].size();
    
    std::string video(video_data_packet, totle_avcc_buf_size);
    
    int8_t avc_packet_type = SrsVideoAvcFrameTraitNALU;
    
    if ((err = context->avc_raw.mux_avc2flv(video, frame_type, avc_packet_type, dts, pts, flv, nb_flv)) != srs_success) 
        ret = srs_error_code(err);
        srs_freep(err);
        return ret;
    
    return 0;

    
/**
 *H264数据可能是一帧包含多个slice
 *FFmpeg对H264数据parse时  相同帧的slice可以被解析到一个AVPacket中
 *如果数据是 sps  pps  idr帧  这种组合  解析结果也会存放到一个AVPacket中
 *
 *1.sps  pps  idr帧
 *先把 sps pps 封装为extra data  再封装flv头 做成一个message发送  之后再发送帧数据
 *2.多slice帧数据发送
 *相同帧的slice 先转为avcc封装  然后整体封装一个flv头  作为一个message发送
 */
int h264_write_raw_single_frame(srs_rtmp_t rtmp, char* frames, int frames_size, char *sei, int sei_size, uint32_t dts, uint32_t pts)

    int ret = ERROR_SUCCESS;
    srs_error_t err = srs_success;
    
    srs_assert(frames != NULL);
    srs_assert(frames_size > 0);
    
    srs_assert(rtmp != NULL);
    Context* context = (Context*)rtmp;
    
    SrsBuffer* stream = new SrsBuffer(frames, frames_size);
    SrsAutoFree(SrsBuffer, stream);
    
    int error_code_return = ret;
    
    // send each frame.
    if (!stream->empty()) 
        
        //关键帧 sps pps idr 一起 这里一起拼成一个message发送
        if(((frames[4]&0x1f) == 7)/* || ((frame[0]&0x1f) == 8)*/)
        
            char* frame = NULL;
            int frame_size = 0;
            if ((err = context->avc_raw.annexb_demux(stream, &frame, &frame_size)) != srs_success) 
                ret = srs_error_code(err);
                srs_freep(err);
                return ret;
            
            
            // ignore invalid frame,
            // atleast 1bytes for SPS to decode the type
            if (frame_size <= 0) 
                return ret;
            
            
            //printf("lidechen_test type=%d\\n", frame[0]&0x1f);
        
            //解析sps 存储到context
            if (context->avc_raw.is_sps(frame, frame_size)) 
                std::string sps;
                if ((err = context->avc_raw.sps_demux(frame, frame_size, sps)) != srs_success) 
                    ret = srs_error_code(err);
                    srs_freep(err);
                    return ret;
                
                if (context->h264_sps != sps) 
                    //更新sps
                    context->h264_sps_changed = true;
                    context->h264_sps = sps;
                
            
            
            //读出pps
            if ((err = context->avc_raw.annexb_demux(stream, &frame, &frame_size)) != srs_success) 
                ret = srs_error_code(err);
                srs_freep(err);
                return ret;
            
            //解析pps 存储到context
            if (context->avc_raw.is_pps(frame, frame_size)) 
                std::string pps;
                if ((err = context->avc_raw.pps_demux(frame, frame_size, pps)) != srs_success) 
                    ret = srs_error_code(err);
                    srs_freep(err);
                    return ret;
                
                
                if (context->h264_pps != pps) 
                    //更新pps
                    context->h264_pps_changed = true;
                    context->h264_pps = pps;
                
            
        
            //如果sps pps中有变化
            if(context->h264_sps_changed || context->h264_pps_changed)

                // 目前测试如果把extradata 直接和关键帧封装到一起 网络发送时管道断开(single13)
                // 参考srs流程目前单独封装extra data 发送
                // send pps+sps before ipb frames when sps/pps changed.
                if ((ret = srs_write_h264_sps_pps(context, dts, pts)) != ERROR_SUCCESS) 
                    return ret;
                
            
        
            char *flv;
            int nb_flv;
            srs_h264_mux_single_frame_to_flv(context, stream, &flv, &nb_flv, sei, sei_size,dts, pts);
            //PrintCharArrayAsHex(flv, nb_flv);
            srs_rtmp_write_packet(context, SRS_RTMP_TYPE_VIDEO, dts, flv, nb_flv);
            
        else if(((frames[4]&0x1f) == 1))
            
            char *flv;
            int nb_flv;
            
            srs_h264_mux_single_frame_to_flv(context, stream, &flv, &nb_flv, sei, sei_size, dts, pts);
            //PrintCharArrayAsHex(flv, nb_flv);
            srs_rtmp_write_packet(context, SRS_RTMP_TYPE_VIDEO, dts, flv, nb_flv);
            
        
    
    
    return error_code_return;

3.rtmp接收数据流程

这里还是以srs为例,看一下rtmp数据接收流程,从另一个角度理解一下花屏的原因。下面会将数据读取到SrsCommonMessage中,而我们发送rtmp数据的时候,也是使用SrsCommonMessage进行封包,下层拆分为chunk进行发送。这里data最终会获取到我们发送的负载数据。

int srs_rtmp_read_packet(srs_rtmp_t rtmp, char* type, uint32_t* timestamp, char** data, int* size)

    ...
    Context* context = (Context*)rtmp;
    
    for (;;) 
        SrsCommonMessage* msg = NULL;
        
        // read from cache first.
        if (!context->msgs.empty()) 
            std::vector<SrsCommonMessage*>::iterator it = context->msgs.begin();
            msg = *it;
            context->msgs.erase(it);
        
        //读取一个SrsCommonMessage数据
        // read from protocol sdk.
        if (!msg && (err = context->rtmp->recv_message(&msg)) != srs_success) 
            ret = srs_error_code(err);
            srs_freep(err);
            return ret;
        
        
        // no msg, try again.
        if (!msg) 
            continue;
        
        
        SrsAutoFree(SrsCommonMessage, msg);
        
        //获取当前SrsCommonMessage中数据类型、时间戳以及payload数据
        // process the got packet, if nothing, try again.
        bool got_msg;
        if ((ret = srs_rtmp_go_packet(context, msg, type, timestamp, data, size, &got_msg)) != ERROR_SUCCESS) 
            return ret;
        
        
        // got expected message.
        if (got_msg) 
            break;
        
    
    
    return ret;

其中 srs_rtmp_go_packet 会将当前message分类,这样外部根据类型去处理负载数据。

int srs_rtmp_go_packet(Context* context, SrsCommonMessage* msg,
    char* type, uint32_t* timestamp, char** data, int* size,
    bool* got_msg
) 
    int ret = ERROR_SUCCESS;
    
    // generally we got a message.
    *got_msg = true;
    
    if (msg->header.is_audio()) 
        //当前为音频数据
        *type = SRS_RTMP_TYPE_AUDIO;
        *timestamp = (uint32_t)msg->header.timestamp;
        *data = (char*)msg->payload;
        *size = (int)msg->size;
        // detach bytes from packet.
        msg->payload = NULL;
     else if (msg->header.is_video()) 
        //当前为视频数据
        *type = SRS_RTMP_TYPE_VIDEO;
        *timestamp = (uint32_t)msg->header.timestamp;
        *data = (char*)msg->payload;
        *size = (int)msg->size;
        // detach bytes from packet.
        msg->payload = NULL;
     else if (msg->header.is_amf0_data() || msg->header.is_amf3_data()) 
        //处理amf数据
        *type = SRS_RTMP_TYPE_SCRIPT;
        *data = (char*)msg->payload;
        *size = (int)msg->size;
        // detach bytes from packet.
        msg->payload = NULL;
     else if (msg->header.is_aggregate()) 
        if ((ret = srs_rtmp_on_aggregate(context, msg)) != ERROR_SUCCESS) 
            return ret;
        
        *got_msg = false;
     else 
        *type = msg->header.message_type;
        *data = (char*)msg->payload;
        *size = (int)msg->size;
        // detach bytes from packet.
        msg->payload = NULL;
    
    
    return ret;

目前我们测试,如果多slice每个nal作为一个message,ffmplay、浏览器这些标准播放器都会花屏,这样看很可能是将slice分开放入了解码器导致,如果我们将通帧slice合并到一个message中发送,这样payload中数据是完整的,直接放入解码器是正常的。

4.H264中如何判断多个slice是否属于一帧

这里引申出另外一个问题,一段H264码流中,如何判断slice是否为同一帧。这里我们可以参考一下ffmpeg的AVParser,如果使用ffmpeg解码H264数据,我们会先将buffer中的数据使用AVParser进行分割,得到AVPacket后放入解码器。这里可以看一下ffmpeg中 h264_parser.c。

static int h264_find_frame_end(H264ParseContext *p, const uint8_t *buf,
                               int buf_size, void *logctx)

    int i, j;
    uint32_t state;
    ParseContext *pc = &p->pc;

    int next_avc = p->is_avc ? 0 : buf_size;
//    mb_addr= pc->mb_addr - 1;
    state = pc->state;
    if (state > 13)
        state = 7;

    if (p->is_avc && !p->nal_length_size)
        av_log(logctx, AV_LOG_ERROR, "AVC-parser: nal length size invalid\\n");

    for (i = 0; i < buf_size; i++) 
        if (i >= next_avc) 
            int nalsize = 0;
            i = next_avc;
            for (j = 0; j < p->nal_length_size; j++)
                nalsize = (nalsize << 8) | buf[i++];
            if (nalsize <= 0 || nalsize > buf_size - i) 
                av_log(logctx, AV_LOG_ERROR, "AVC-parser: nal size %d remaining %d\\n", nalsize, buf_size - i);
                return buf_size;
            
            next_avc = i + nalsize;
            state    = 5;
        

        if (state == 7) 
            i += p->h264dsp.startcode_find_candidate(buf + i, next_avc - i);
            if (i < next_avc)
                state = 2;
         else if (state <= 2) 
            if (buf[i] == 1)
                state ^= 5;            // 2->7, 1->4, 0->5
            else if (buf[i])
                state = 7;
            else
                state >>= 1;           // 2->1, 1->0, 0->0
         else if (state <= 5) 
            int nalu_type = buf[i] & 0x1F;
            if (nalu_type == H264_NAL_SEI || nalu_type == H264_NAL_SPS ||
                nalu_type == H264_NAL_PPS || nalu_type == H264_NAL_AUD) 
                if (pc->frame_start_found) 
                    i++;
                    goto found;
                
             else if (nalu_type == H264_NAL_SLICE || nalu_type == H264_NAL_DPA ||
                       nalu_type == H264_NAL_IDR_SLICE) 
                state += 8;
                continue;
            
            state = 7;
         else 
            p->parse_history[p->parse_history_count++] = buf[i];
            if (p->parse_history_count > 5) 
                unsigned int mb, last_mb = p->parse_last_mb;
                GetBitContext gb;

                init_get_bits(&gb, p->parse_history, 8*p->parse_history_count);
                p->parse_history_count = 0;
                mb= get_ue_golomb_long(&gb);
                p->parse_last_mb = mb;
                if (pc->frame_start_found) 
                    if (mb <= last_mb)
                        goto found;
                 else
                    pc->frame_start_found = 1;
                state = 7;
            
        
    
    pc->state = state;
    if (p->is_avc)
        return next_avc;
    return END_NOT_FOUND;

found:
    pc->state             = 7;
    pc->frame_start_found = 0;
    if (p->is_avc)
        return next_avc;
    return i - (state & 5) - 5 * (state > 7);

这段代码状态机写的气壮山河...... 我们先放过这段,着重看这里,如果新的宏块位置不大于上次的 则找到一个完整帧。

unsigned int mb, last_mb = p->parse_last_mb;
GetBitContext gb;
//读取slice头  无符号指数哥伦布解析 获取头部宏块位置
init_get_bits(&gb, p->parse_history, 8*p->parse_history_count);
p->parse_history_count = 0;
mb= get_ue_golomb_long(&gb);
//记录本次哄块位置
p->parse_last_mb = mb;
if (pc->frame_start_found) 
    //如果新的宏块位置不大于上次的 则找到一个完整帧
    if (mb <= last_mb)
       goto found;
     else
pc->frame_start_found = 1;
state = 7;

下面我们找个帧头手动计算一下宏块位置

65 88 82 2f de 08 56
0x88  0x82  ->  0b 1000 1000 1000 0010

二进制首位为1 ,无符号指数哥伦布 2的0次方-1+0 = 0,也就是宏块0是首宏块。后面同帧slice头部位置为继续增加,遇到下一个0则说明前面是一个完整帧。这样的完整帧数据放入解码器才能正确解码。

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

以上是关于rtmp H264多Slice封装学习笔记的主要内容,如果未能解决你的问题,请参考以下文章

rtmp H264多Slice封装学习笔记

h264多slice

H264软编码导致画面切换时不流畅

H264软编码导致画面切换时不流畅

RTMP推流方案总结

JavaCV音视频开发宝典:视频转码和转封装有什么区别?使用rtsp拉流转推到rtmp案例来讲一下转码和转封装实现的区别