IJKPlayer是一个非常优秀的播放器,支持rtmp、rtsp、http等协议直播,也支持Android、iOS跨平台使用。我在使用IJKPlayer做rtsp直播时,发现分辨率在达到1080P甚至是4K时,由于数据量比较大,有时出现花屏,影响用户体验。那么,我们需要做的是避开花屏,增大拉流缓冲区防止溢出、丢掉不完整帧、不渲染解码出错帧。
一、增大拉流缓冲区
我们默认是使用udp去拉流,在udp.c文件定义缓冲区大小为UDP_MAX_PKT_SIZE,默认值是65536。在这里,我们把它扩大10倍,改为65536*10。这样能够更大程度保证,在高分辨率时,拉流缓冲区不溢出。
二、丢掉不完整帧
现在主流是h264编解码,所以拉流时会经历rtp拆包、h264解析、h264组帧过程。在这些过程中,如果出现丢包,往往会导致视频帧不完整,FFmpeg也会打印error日志。我们需要在这些出错环节作帧错误标记,用于渲染视频帧时作判断,如果发生帧错误,丢掉整个GOP的帧。下面是出现花屏时,根据FFmpeg打印的日志去寻找对应文件。
1、rtp解析出错
在rtpdec.c文件的rtp_parse_queued_packet方法里,有missed %d packets的日志,在这里把帧错误标记为1。
if (!has_next_packet(s)){
av_log(s->ic, AV_LOG_WARNING,
"RTP: missed %d packets\n", s->queue->seq - s->seq - 1);
//the flag of error frame
frame_err = 1;
}
2、在h264_parser.c文件的parse_nal_units方法,有non-existing PPS、non-existing SPS、missing pictures日志,前两个是由于没有找到SPS、PPS,后面的是数据包不完整提示,在这里作帧错误标记。
if (!p->ps.pps_list[pps_id]) {
av_log(avctx, AV_LOG_ERROR,"non-existing PPS %u referenced\n", pps_id);
goto fail;
}
if (!p->ps.sps_list[p->ps.pps->sps_id]) {
av_log(avctx, AV_LOG_ERROR,"non-existing SPS %u referenced\n", p->ps.pps->sps_id);
goto fail;
}
/* didn't find a picture! */
av_log(avctx, AV_LOG_ERROR, "missing picture in access unit with size %d\n", buf_size);
3、在h264_parse.c文件的ff_h264_check_intra4x4_pred_mode方法中,有top block unavailable、left block unavailable日志,在ff_h264_check_intra_pred_mode方法中,有out of range intra chroma等日志,需要作帧错误标记。
if (status < 0) {
av_log(logctx, AV_LOG_ERROR,"top block unavailable for requested intra mode %d\n",status);
return AVERROR_INVALIDDATA;
}
if (status < 0) {
av_log(logctx, AV_LOG_ERROR,"left block unavailable for requested intra4x4 mode %d\n",status);
return AVERROR_INVALIDDATA;
}
if (mode > 3U) {
av_log(logctx, AV_LOG_ERROR,"out of range intra chroma pred mode\n");
return AVERROR_INVALIDDATA;
}
4、在h264_cavlc.c文件的decode_residual方法中,有corrupted macroblock、Invalid level prefix、negative number日志,在ff_h264_decode_mb_cavlc方法中,有mb_type %d in %c slice too large日志,需要作帧错误标记。
if(total_coeff > (unsigned)max_coeff) {
av_log(h->avctx, AV_LOG_ERROR, "corrupted macroblock %d %d (total_coeff=%d)\n", sl->mb_x, sl->mb_y, total_coeff);
return -1;
}
if(prefix > 25+3){
av_log(h->avctx, AV_LOG_ERROR, "Invalid level prefix\n");
return -1;
}
if(zeros_left<0){
av_log(h->avctx, AV_LOG_ERROR, "negative number of zero coeffs at %d %d\n", sl->mb_x, sl->mb_y);
return -1;
}
if(mb_type > 25){
av_log(h->avctx, AV_LOG_ERROR, "mb_type %d in %c slice too large at %d %d\n", mb_type, av_get_picture_type_char(sl->slice_type), sl->mb_x, sl->mb_y);
return -1;
}
5、在error_resilience.c文件的ff_er_add_slice方法中,有internal error日志,需要作帧错误标记。
if (start_i > end_i || start_xy > end_xy) {
av_log(s->avctx, AV_LOG_ERROR,"internal error, slice end before start\n");
return;
}
6、在h264_slice.c文件的decode_slice方法中,有error while decoding MB错误日志,,在h264_slice_header_parse方法中,有Frame num change from日志,需要作帧错误标记。
if (h->poc.frame_num != sl->frame_num) {
av_log(h->avctx, AV_LOG_ERROR, "Frame num change from %d to %d\n",h->poc.frame_num, sl->frame_num);
return AVERROR_INVALIDDATA;
}
if (ret < 0) {
av_log(h->avctx, AV_LOG_ERROR,"error while decoding MB %d %d\n", sl->mb_x, sl->mb_y);
//TODO:flag of decode frame error, add by xufulong
decode_err = 1;
er_add_slice(sl, sl->resync_mb_x, sl->resync_mb_y, sl->mb_x,sl->mb_y, ER_MB_ERROR);
return ret;
}
最后在ff_ffplay.c文件的read_thread方法中,判断有没帧错误,如果出现帧错误就不入队列。
//if frame has occured error, don't enqueue
if(!frame_err){
packet_queue_put(&is->videoq, pkt);
}else {//release the packet when frame error during total GOP
av_packet_unref(pkt);
}
三、不渲染解码出错帧
在ffplay_video_thread解码线程里,如果出现解码出错,作对应标记,不渲染错误帧。
ret = get_video_frame(ffp, frame);
if (ret == 0){//decode error
//decode frame error, set the flag
decode_err = 1;
continue;
}else if(ret == -1){//can't get raw frame this time
continue;
}else if (ret < -1){//other error
goto the_end;
}
//decode error, don't display
if(decode_err){
continue;
}
通过以上修改,基本上不会有花屏了。当然如果是h265流,需要去修改hevc对应的文件作标记。