问题现象
iOS竖屏下,编码的码流在Android, Windows的终端解码偶现右侧出现绿线或黑条。
问题定位
解码端排查
当看到这个现象只在某些终端出现时,第一感觉应该是解码端的问题。因此,我们首先对解码端进行排查。
第一步,保存解码端收到的码流。
第二步,使用StreamEye分析码流,发现解码端收到的码流就存在了绿条,并且出现绿条的视频码流的分辨率为180x320和540x960。
因为解码端收到的码流就已经有问题了,所以问题又转到了编码端来排查。
编码端排查
第一步,保存编码后的视频。排除中间服务器的影响,这也是我们定位问题的原则,首先确定问题是不是在自己的模块,不能别人说是你的问题就是你的问题,要有自己的判断。
第二步,使用StreamEye分析码流,发现编码后的码流确实也存在绿条。查看编码参数的配置,分析哪些可能有关系,但是一直未发现问题。接下只能编码的前一个模块走,分析采集模块了。
第三步,保存采集后的yuv数据。
第四步,使用7yuv工具进行分析yuv数据,发现也是有绿条。接下来就把主要精力放在了采集这个模块。但是排查采集模块的时,走了很多弯路,但是也收获了不少。
采集模块排查
首先第一感觉是yuv中为什么会出现绿条呢?后来突然想起苹果有一个方法CVPixelBufferGetBytesPerRowOfPlane获取的值与yuv的宽度不一样,这个函数获取到数据包含了一些填充数据,目的是为了内存对齐。由于之前对这个了解的不是很清楚,然后网上搜索了一些关于这方面的内容,得到了一个跨距(stride)的概念。跨距是指图像中的一行图像数据所占的存储空间的长度,它是一个大于等于图像宽度的内存对齐的长度。这样每次读取的时候以此为基准读取数据的时候就能内存对齐。参考链接:https://www.jianshu.com/p/68e05ad85490。不同手机内存对齐的位数是不一样的,测试发现iPhone6是16位对齐,iPhone6s是64位对齐。因此,yuv的右侧有绿条或黑条也就是正常了,这时问题又陷入了僵局。
只能猜想是不是系统编码器内部缩放yuv时,没处理好跨距。现在只能怀疑一切了。因此就想到使用libyuv先自己做缩放然后,再送人编码器。
测试代码如下:
//缩放yuv到当前编码分辨率
/*
CVPixelBufferLockBaseAddress(imageBuf, kCVPixelBufferLock_ReadOnly);
const uint8_t *src_y = CVPixelBufferGetBaseAddressOfPlane(imageBuf, 0);
int src_stride_y = (int)CVPixelBufferGetBytesPerRowOfPlane(imageBuf, 0);
const uint8_t *src_uv = CVPixelBufferGetBaseAddressOfPlane(imageBuf, 1);
int src_stride_uv = (int)CVPixelBufferGetBytesPerRowOfPlane(imageBuf, 1);
int src_width = (int)CVPixelBufferGetWidth(imageBuf);
int src_height = (int)CVPixelBufferGetHeight(imageBuf);
CVPixelBufferUnlockBaseAddress(imageBuf, kCVPixelBufferLock_ReadOnly);
OSStatus status;
CVPixelBufferRef pixelBuffer = NULL;
int dst_width = _width;
int dst_height = _height;
status = CVPixelBufferCreate(kCFAllocatorDefault, dst_width, dst_height, kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange, NULL, &pixelBuffer);
CVPixelBufferLockBaseAddress(pixelBuffer, kCVPixelBufferLock_ReadOnly);
int dst_stride_y = (int)CVPixelBufferGetBytesPerRowOfPlane(pixelBuffer, 0);
int dst_stride_uv = (int)CVPixelBufferGetBytesPerRowOfPlane(pixelBuffer, 1);
uint8_t *dst_y = CVPixelBufferGetBaseAddressOfPlane(pixelBuffer, 0);
uint8_t *dst_uv = CVPixelBufferGetBaseAddressOfPlane(pixelBuffer, 1);
int ret = NV12Scale(src_y, src_stride_y, src_uv, src_stride_uv, src_width, src_height, dst_y, dst_stride_y, dst_uv, dst_stride_uv, dst_width, dst_height, kFilterBox);
if (ret != 0) {
ILog(@"[%@] encodeFrame scale fail, [%dx%d]->[%dx%d]",self, src_width, src_height, dst_width, dst_height);
}
CVPixelBufferUnlockBaseAddress(pixelBuffer, kCVPixelBufferLock_ReadOnly);
但是,经过测试发现libyuv缩放编码后仍然有绿条。
问题又一次陷入了僵局,没有头绪。后来在用7yuv工具分析时,突然看到缩放前的yuv的真实数据宽度好像是704(其实是当时看错了),并不是720,这让问题又有了一丝希望。
冲着这个方向,怀疑是不是由于横竖屏旋转(横屏情况下是正常的)时,导致yuv的真实数据出现了问题。但是通过启动时直接设置为竖屏对比分析yuv数据,也是有问题,故排除了苹果内部旋转。其实这个验证是不严谨的,因为setSessionPreset时,只支持横屏的格式,比如,AVCaptureSessionPreset1280x720。而我们只是启动时将AVCaptureConnection的setVideoOrientation设置为了AVCaptureVideoOrientationPortrait这个,其内部应该还是做了旋转。严谨的方式应该是使用libyuv自己做旋转,才能排查。
但是在这个过程中突然发现yuv的有效数据是正常的,就是720宽,而跨距是768。开始由于对7yuv工具上边的标尺使用不当,后来发现点击标尺上的箭头可以看到当前的刻度值是多少,哎,悲剧呀。
采集模块出来的数据,有效数据的宽度正确,有效数据到跨距结束的部分,本来就应该被填充一些无效数据,可能就是绿色或黑色。这下问题排除是采集模块的问题了,这是只能又转向编码模块了。
编码模块排查
继续用7yuv分析yuv数据,发现一个细节yuv缩放后,180到184这个段的数据是纯绿色或纯黑色的,184到192的数据是不确定的,有绿色,有黑色。且184正好是8的倍数。怀疑是不是送入编码器的有效数据需要是8的倍数呢?
因此,尝试将手动缩放的分辩率改为184x320,即缩放后的yuv是184x320,进行测试,发现编码后还是存在绿条或黑条。
最后,想起了编码分辨率需要是8的倍数的说法,尝试将编码分辨率改为184x320,测试发现编码后的数据没有绿条了。这是由于
编码h264视频流的时候,h264的编码宏块大小16x16,帧内编码时宏块还拆成8x8子块。
总结
这个排查过程中,有以下几个收获:
- 对跨距的理解更深刻了,之前只是知道一个yuv真实数据和填充数据的事情。
- 对编码器的分辨率宽度设置一定是8的倍数或16的倍数,这个概念记忆深刻。
- 对7yuv工具的使用更加熟悉。