ijkplayer-花屏问题分析

2,762 阅读5分钟

今天遇到个应用在S2机器上拖动播放随机花屏的问题,其他机子都没有。那么如何分析花屏问题呢,我这里主要分析点播和本地文件播放导致的花屏,并不涉及直播花屏分析?

分析花屏问题无外乎从==解复用->解码->显示==这三步开始排查。(好吧,这个花屏问题真的让人蛋疼啊)

1.排查是否显示问题

先从显示开始排查,可以直接把解码后的数据编码成图片然后取出来,如果取出来的图片是花屏的,那么就继续往解码以及解复用开始排查。如果取出来的图片不是花屏的,那么就可以判断为显示导致的花屏。

先看下在jik中对解码后的帧编码图片取到本地的实现方式:

我们需要先获取到软解码后的帧数据,在ff_ffplay的ffplay_video_thread函数中,这里直接取。

int i = 0;//作为编码成图片的帧序列号,函数外
...
...
for (;;) {
//软解码获取到解码后的视频帧
        ret = get_video_frame(ffp, frame);
        if (ret < 0)
            goto the_end;
        if (!ret)
            continue;
		//add by hxk,花屏检测的实现方式
		#if 0
			AVFrame *pFrameRGB;
			pFrameRGB = av_frame_alloc();//申请内存
			if(pFrameRGB == NULL) {
				av_log(NULL, AV_LOG_ERROR, "pFrameRGB malloc failed!\n");
				return;
			}
			int numBytes = avpicture_get_size(AV_PIX_FMT_RGB24, is->viddec.avctx->width, is->viddec.avctx->height);
			uint8_t * buffer = (uint8_t *)av_malloc(numBytes * sizeof(uint8_t));
	    	avpicture_fill((AVPicture*)pFrameRGB, buffer, AV_PIX_FMT_RGB24, is->viddec.avctx->width, is->viddec.avctx->height);
			struct SwsContext* img_convert_ctx;
			img_convert_ctx = sws_getContext(is->viddec.avctx->width,is->viddec.avctx->height, frame->format,
				is->viddec.avctx->width, is->viddec.avctx->height, AV_PIX_FMT_RGB24, SWS_BICUBIC, NULL, NULL, NULL);
			sws_scale(img_convert_ctx,  (const uint8_t * const *)frame->data, (const int *)frame->linesize, 0, is->viddec.avctx->height, 
					pFrameRGB->data, pFrameRGB->linesize);
			
			SaveFrame(pFrameRGB, is->viddec.avctx->width, is->viddec.avctx->height, i++);
			av_free(buffer);
			av_free(pFrameRGB);
			sws_freeContext(img_convert_ctx);
	#endif 
	

        if (ffp->get_frame_mode) {
            if (!ffp->get_img_info || ffp->get_img_info->count <= 0) {
                av_frame_unref(frame);
                continue;
            }

            last_dst_pts = dst_pts;

            if (dst_pts < 0) {
                dst_pts = ffp->get_img_info->start_time;
            } else {
                dst_pts += (ffp->get_img_info->end_time - ffp->get_img_info->start_time) / (ffp->get_img_info->num - 1);
            }

            pts = (frame->pts == AV_NOPTS_VALUE) ? NAN : frame->pts * av_q2d(tb);
            pts = pts * 1000;
            if (pts >= dst_pts) {
                while (retry_convert_image <= MAX_RETRY_CONVERT_IMAGE) {
                    ret = convert_image(ffp, frame, (int64_t)pts, frame->width, frame->height);
                    if (!ret) {
                        convert_frame_count++;
                        break;
                    }
                    retry_convert_image++;
                    av_log(NULL, AV_LOG_ERROR, "convert image error retry_convert_image = %d\n", retry_convert_image);
                }

                retry_convert_image = 0;
                if (ret || ffp->get_img_info->count <= 0) {
                    if (ret) {
                        av_log(NULL, AV_LOG_ERROR, "convert image abort ret = %d\n", ret);
                        ffp_notify_msg3(ffp, FFP_MSG_GET_IMG_STATE, 0, ret);
                    } else {
                        av_log(NULL, AV_LOG_INFO, "convert image complete convert_frame_count = %d\n", convert_frame_count);
                    }
                    goto the_end;
                }
            } else {
                dst_pts = last_dst_pts;
            }
            av_frame_unref(frame);
            continue;
        }

            duration = (frame_rate.num && frame_rate.den ? av_q2d((AVRational){frame_rate.den, frame_rate.num}) : 0);
            pts = (frame->pts == AV_NOPTS_VALUE) ? NAN : frame->pts * av_q2d(tb);
            ret = queue_picture(ffp, frame, pts, duration, frame->pkt_pos, is->viddec.pkt_serial);
            av_frame_unref(frame);
#if CONFIG_AVFILTER
        }

下面实现保存帧成ppm图片到本地文件中

/**
*保存yuv成ppm图片格式到本地
**/
void SaveFrame(AVFrame *pFrame, int width, int height, int64_t iFrame)
{
	av_log(NULL, AV_LOG_ERROR, "saveFrame:width:%d,height:%d,iFrame:%lld\n",width,height,iFrame);
	FILE *pFile;
	char szFilename[32];
	sprintf(szFilename, "sdcard/frame%lld.ppm", iFrame);
	pFile = fopen(szFilename,"wb+");
	if (pFile == NULL) {
		av_log(NULL, AV_LOG_ERROR, "pFile is null!\n");
		return;
	}
	if (pFrame == NULL) {
		av_log(NULL, AV_LOG_ERROR, "pFrame data is null!\n");
		return;
	}
	if (pFrame->data == NULL) {
		av_log(NULL, AV_LOG_ERROR, "pFrame data is null!\n");
		return;
	}
	fprintf(pFile,"P6\n%d %d\n255\n",width,height);
	//写文件到本地
	for (int y = 0;y < height;y++) {
		fwrite(pFrame->data[0] + y * pFrame->linesize[0],1,width * 3,pFile);
	}
	fclose(pFile);
}

这样就可以直接查看图片判断是否是显示的问题了。 如果不是显示的问题,下面可以去查看解复用器,是否是解复用器导致的花屏。

2.解复用导致的花屏

由于我们的视频是自定义的视频容器,存在一些兼容性问题。 可以查看解复用器中对关键帧的处理,以及seek时查找关键帧是否正确。

这里以我遇到的举例,seek时花屏,发现在ffplay上seek没有花屏,但是在ijk上seek花屏,最后定位到查找关键帧的索引错了。(rk机子让人蛋疼啊)。最后处理的是不是box中关键帧的enrty的flag需要置0,不然在rk机器上会被随机赋值成其他值,导致seek的时候判断关键帧的逻辑取了错误的关键帧,导致花屏。

static int imvoc_read_trak(imvoc_context *c, AVIOContext *pb, imvoc_box_t atom)
{
    int ret = imvoc_read_default(c, pb, atom);
    if (ret== 0)
    {
       AVStream *st = c->fc->streams[c->fc->nb_streams-1];
       st->nb_index_entries = c->pCurTrack->spsz.entryCount;
       st->index_entries_allocated_size =  st->nb_index_entries*sizeof(AVIndexEntry);
       st->index_entries = av_malloc(st->index_entries_allocated_size);
       if (st->index_entries == NULL)
       {
           av_log(c->fc, AV_LOG_ERROR, "imvoc_read_trak av_malloc error\n");
           return AVERROR(ENOMEM);
       }
       else
       {
           int64_t curTimestamp = 0;
           int lastraacIndex = 1;
           unsigned int nextkeyFrame = 0;

           for(int i = 0; i < st->nb_index_entries; i++)
           {
               AVIndexEntry *pEntry = &st->index_entries[i];
               pEntry->size = c->pCurTrack->spsz.sampleEntry[i];
               pEntry->pos = c->pCurTrack->spof.sampleEntry[i];

               pEntry->timestamp = curTimestamp;
			   //fix by hxk,Solve audio and video synchronization
			   if(STCODECP(st)->codec_type == AVMEDIA_TYPE_VIDEO)
			   {
			   	    //hxk:some old video should use thie method to get pts,old video's format is mepeg4
			   		if(st->codecpar->codec_id == AV_CODEC_ID_MPEG4){
						if(c->pCurTrack == NULL || c->pCurTrack->frameRate == 0){
							av_log(c->fc, AV_LOG_ERROR, "c->pCurTrack is null or pCurTrack->frameRate == 0!\n");
           					return AVERROR(ENOMEM);
						}
			   			curTimestamp = (int64_t)i * 1000 / c->pCurTrack->frameRate;
			   		} 
					else 
					{
						//av_log(c->fc, AV_LOG_ERROR, "no mpeg4!\n");
			   			curTimestamp += c->pCurTrack->spdt.sampleEntry[i];
			   		}
			   	} 
				else 
				{
				   curTimestamp += c->pCurTrack->spdt.sampleEntry[i];
			   }
			   //fix end
             //  curTimestamp += c->pCurTrack->spdt.sampleEntry[i];

               if (STCODECP(st)->codec_type == AVMEDIA_TYPE_AUDIO)
               {
                   pEntry->flags = AVINDEX_KEYFRAME;
               }
               else if(STCODECP(st)->codec_type == AVMEDIA_TYPE_VIDEO && i == nextkeyFrame)
               {
                   pEntry->flags = AVINDEX_KEYFRAME;
                   if(c->pCurTrack->raac.sampleEntry != NULL &&
                      lastraacIndex < lastraacIndex < c->pCurTrack->raac.entryCount)
                   {
                        nextkeyFrame = c->pCurTrack->raac.sampleEntry[lastraacIndex++];
                   }
               } else {
               	  pEntry->flags = 0;//fix by hxk,如果不置0,在一些机器上会被设置成其他值,导致seek时取关键帧错误
               	}
           }
         //  av_log(c->fc, AV_LOG_INFO, "timestap:%d\n", (int)st->index_entries[st->nb_index_entries-1].timestamp);
       }
    }
    return 0;
}

3.解码器导致的花屏

这里区分为软解码和硬解码,软解码导致的花屏目前没遇到,可以先看硬解码导致的花屏问题。 这里主要集中在不同的解码器中赋值给mediacodec的sps和pps的值不对,以及mediacodec相关的参数设置不对导致解码花屏。

4.渲染导致的花屏

渲染导致的花屏一般有:

  • 解码后的视频帧没有适配好,没有做针对屏幕大小做对应的缩放处理等
  • 渲染格式不支持等,如解码后的视频是I420的,但是渲染实现只支持NV21格式等
  • 纹理buffer没清除等导致的渲染花屏。

5.其他情况导致的一些花屏

  1. 码率过低,导致整个全局视频花屏,需要调高编码视频的码率。
  2. 视频参数问题,比如当视频源修改过视频参数(如从720P修改1080P),此时客户端用于解码的SPS&PPS如果没有重新获取的话,就会出现整个画面花屏的现象。这种花屏的现象会一直持续下去,不会随着时间而恢复正常画面。
  3. P帧丢失,I帧正常丢失P帧的情况下,画面的大部分区域是正常的,只有在发生变化的那部分区域会存在局部花屏。

相关文章收集

直播问题分析总结 -- 花屏&绿屏