在使用src_librtmp转推H264数据时,拉流端观看出现了花屏问题。经过排查发现客户端X264编码时如果设置了分片,转推为rtmp就会导致花屏,关闭分片相关设置视频正常。在转推H264前将数据写入本地,播放正常,播放转推后的rtmp花屏,ffplay会报错。这里推断是rtmp封装问题导致了花屏,下面首先需要对于这种一帧H264视频中包含多个Slice的情况,应当如何封装。
这里们尝试使用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 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; iavc_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;
}
这里还是以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::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中数据是完整的,直接放入解码器是正常的。
这里引申出另外一个问题,一段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则说明前面是一个完整帧。这样的完整帧数据放入解码器才能正确解码。