今天在使用 FFMpeg 解码 iPad 过来的视频流时,有一个非常奇怪的现象,即对于分辨率为 406*720 和 450*720 的图像来说,解码一切正常,但是把 YUV420 的数据转为 RGBA 显示出来时,图像的最右边会有一条几个像素的黑竖条,如下图所示:

./images/ffmpeg-black-bar2.jpg

碰到这个问题,通常的解决思路如下。

第一,检查一下解码是否有错误,这个好办,只要解码不返回错误码就是正常。

第二,如果解码没有问题的话,仔细查看各步的图片格式转化操作是否有问题。

1. 先看看解码出来的 yuv 数据是否就有这条黑边。可以直接把 yuv 保存成二进制文件,然后用 yuvplayer 播放。

如果是使用 FFMpeg 解码,则可以使用如下的代码来保存 yuv (平面格式)数据:

<pre class="src src-cpp"><span style="color: #99968b">//</span><span style="color: #99968b">&#20445;&#23384;yuv&#25968;&#25454;</span>

static int fcount = 0; char yuvdumpName[255];

sprintf(yuvdumpName, “E:/temp/yuv/%d.yuv”, fcount); FILE *fp = fopen(yuvdumpName, “ab”); if (fp) {

<span style="color: #92a65e;font-weight: bold">char</span>* <span style="color: #cae682">yuv</span> = (<span style="color: #92a65e;font-weight: bold">char</span>*)src_frame-&gt;data[0];
<span style="color: #92a65e;font-weight: bold">int</span> <span style="color: #cae682">stride</span> = src_frame-&gt;linesize[0];

<span style="color: #8ac6f2;font-weight: bold">for</span>(<span style="color: #92a65e;font-weight: bold">int</span> <span style="color: #cae682">y</span>=0; y&lt;720; y++) {
    fwrite(yuv+stride*y, 1, 540, fp);
}

<span style="color: #92a65e;font-weight: bold">char</span>* <span style="color: #cae682">udata</span> = (<span style="color: #92a65e;font-weight: bold">char</span>*)src_frame-&gt;data[1];
<span style="color: #92a65e;font-weight: bold">int</span> <span style="color: #cae682">ustride</span> = src_frame-&gt;linesize[1];

<span style="color: #8ac6f2;font-weight: bold">for</span>(<span style="color: #92a65e;font-weight: bold">int</span> <span style="color: #cae682">u</span>=0; u&lt;720/2; u++) {
    fwrite(udata+u*ustride, 1, 540/2, fp);
}

<span style="color: #92a65e;font-weight: bold">char</span>* <span style="color: #cae682">vdata</span> = (<span style="color: #92a65e;font-weight: bold">char</span>*)src_frame-&gt;data[2];
<span style="color: #92a65e;font-weight: bold">int</span> <span style="color: #cae682">vstride</span> = src_frame-&gt;linesize[2];
<span style="color: #8ac6f2;font-weight: bold">for</span>(<span style="color: #92a65e;font-weight: bold">int</span> <span style="color: #cae682">v</span>=0; v&lt;720/2; v++) {
    fwrite(vdata+v*vstride, 1, 540/2, fp);
}

fclose(fp);
fcount++;

}

<p>
  在我这里有一个特别奇怪的现象,分辨是 540*720,但是解码后得到的 YUV 的 stridesize 是: 576, 288, 288。经过查找资料,原来为了方便各种格式转换及效率,通常是把分辨率的宽度进行了 16 倍整数化,方便硬件加速。
</p>

<p>
  在我查看了 yuv 图片后,还是一切正常,真是怪事,只能继续找原因了。
</p></p>

2. 既然 yuv 数据没有黑边,则把 sws_scale 转成 BGRA 数据后的图片保存成位图,看是否有黑边。

可以使用如下的代码来保存 BGRA 的数据:

<pre class="src src-cpp"><span style="color: #92a65e;font-weight: bold">void</span> <span style="color: #cae682">saveBmpFile</span>(<span style="color: #92a65e;font-weight: bold">uint8_t</span> *<span style="color: #cae682">src</span>,<span style="color: #92a65e;font-weight: bold">int</span> <span style="color: #cae682">srcW</span>,<span style="color: #92a65e;font-weight: bold">int</span> <span style="color: #cae682">srcH</span>,<span style="color: #92a65e;font-weight: bold">int</span> <span style="color: #cae682">srcStride</span>)

{ static int fileIndex = 0;

<span style="color: #92a65e;font-weight: bold">char</span> <span style="color: #cae682">filename</span>[256];
sprintf(filename,<span style="color: #95e454">"e:\\temp\\yuv\\bmp\\%d.bmp"</span>, fileIndex++);
<span style="color: #92a65e;font-weight: bold">BITMAPINFOHEADER</span> <span style="color: #cae682">bih</span>; 
<span style="color: #92a65e;font-weight: bold">BITMAPFILEHEADER</span> <span style="color: #cae682">bhh</span>;  
<span style="color: #92a65e;font-weight: bold">int</span> <span style="color: #cae682">widthStep</span> = srcStride; <span style="color: #99968b">//</span><span style="color: #99968b">(((src-&gt;m_width * 24) + 31) & (~31)) / 8 ;  </span>
bih.biSize=40;       <span style="color: #99968b">// </span><span style="color: #99968b">header size  </span>
bih.biWidth= srcW;
bih.biHeight= abs(srcH);
bih.biPlanes=1;  
bih.biBitCount=32;     <span style="color: #99968b">// </span><span style="color: #99968b">RGB encoded, 24 bit  </span>
bih.biCompression=BI_RGB;   <span style="color: #99968b">// </span><span style="color: #99968b">no compression</span>
bih.biSizeImage=srcStride*abs(srcH);
bih.biXPelsPerMeter=0;  
bih.biYPelsPerMeter=0;  
bih.biClrUsed=0;  
bih.biClrImportant=0;   
bhh.bfType = ((WORD) (<span style="color: #95e454">'M'</span> &lt;&lt; 8) | <span style="color: #95e454">'B'</span>);  <span style="color: #99968b">//</span><span style="color: #99968b">'BM'  </span>
bhh.bfSize = (<span style="color: #92a65e;font-weight: bold">DWORD</span>)<span style="color: #8ac6f2;font-weight: bold">sizeof</span>(BITMAPFILEHEADER) + (<span style="color: #92a65e;font-weight: bold">DWORD</span>)<span style="color: #8ac6f2;font-weight: bold">sizeof</span>(BITMAPINFOHEADER) + widthStep * abs(srcH);
bhh.bfReserved1 = 0;  
bhh.bfReserved2 = 0;  
bhh.bfOffBits = (<span style="color: #92a65e;font-weight: bold">DWORD</span>)<span style="color: #8ac6f2;font-weight: bold">sizeof</span>(BITMAPFILEHEADER) + (<span style="color: #92a65e;font-weight: bold">DWORD</span>)<span style="color: #8ac6f2;font-weight: bold">sizeof</span>(BITMAPINFOHEADER);

<span style="color: #92a65e;font-weight: bold">FILE</span> *<span style="color: #cae682">fp</span> = fopen(filename, <span style="color: #95e454">"wb"</span>);
<span style="color: #8ac6f2;font-weight: bold">if</span>( fp ) {
    src = src + (abs(srcH)-1)*srcStride;
    fwrite(&bhh,1,<span style="color: #8ac6f2;font-weight: bold">sizeof</span>(bhh),fp);
    fwrite(&bih,1,<span style="color: #8ac6f2;font-weight: bold">sizeof</span>(bih),fp);
    <span style="color: #8ac6f2;font-weight: bold">for</span>(<span style="color: #92a65e;font-weight: bold">int</span> <span style="color: #cae682">y</span>=0; y&lt;abs(srcH); y++) {
        fwrite(src,1,srcW*4,fp);
        src -= srcStride;
    }
    fclose(fp);
}

}

<p>
  如果使用了 Qt 库,也可以直接这样保存图片:
</p>

<pre class="src src-cpp"><span style="color: #92a65e;font-weight: bold">AVFrame</span> *<span style="color: #cae682">pFrameRGB</span> = avcodec_alloc_frame();

//some decode operation

static int index = 0; QImage image( pFrameRGB->data[0], codec->width, codec->height, QImage::Format_RGB32 ); image = image.copy(); image.save(QString(“E:/temp/yuv/jpg/%1.jpg”).arg(index)); index++;

<p>
  在我这里,恰恰是第2步使用 sws_scale 转换图片格式后出现的黑边。我原来 sws_scale 的使用方法如下:
</p>

<pre class="src src-cpp"><span style="color: #92a65e;font-weight: bold">SwsContext</span> *<span style="color: #cae682">img_convert_ctx_temp</span>;

img_convert_ctx_temp = sws_getContext( codec->width, codec->height, codec->pix_fmt, dst_w, dst_h, (PixelFormat)dst_fmt, SWS_BICUBIC, NULL, NULL, NULL);

sws_scale(img_convert_ctx_temp, src_frame->data, src_frame->linesize, 0, codec->height, pFrameRGB->data, pFrameRGB->linesize);

<p>
  事实上问题就出在这里,是 sws_getContext 这个函数的参数需要带上 SWS_ACCURATE_RND,即修改成如下方式:
</p>

<pre class="src src-cpp"><span style="color: #92a65e;font-weight: bold">SwsContext</span> *<span style="color: #cae682">img_convert_ctx_temp</span>;

img_convert_ctx_temp = sws_getContext( codec->width, codec->height, codec->pix_fmt, dst_w, dst_h, (PixelFormat)dst_fmt, SWS_BICUBIC | SWS_ACCURATE_RND, NULL, NULL, NULL);

<p>
  但是加了这个参数后,运算速度就变得巨慢了。
</p>

<p>
  有一个更好的解决办法,就是在解码后把图片的 yuv 格式转成 BGRA 格式之前,先考虑一下设置 RGBA 格式的图像的宽度是 16 的倍数:
</p>

<pre class="src src-cpp"><span style="color: #92a65e;font-weight: bold">PixelFormat</span> <span style="color: #cae682">dst_fmt</span> = PIX_FMT_BGRA;

int dst_w = (codec->width/16)*16; int dst_h = codec->height;

<p>
  这样在你以后用 sws_scale 时就可以直接使用 dst_w 这样的16的倍数来进行操作。这样,黑边的问题就解决了,黑边得到了裁剪,并且并不会耗费大量的计算时间,不过缺点是图像的分辨率也变小了。*最佳的解决办法,是在压缩流的输出端就保证分辨率的宽度值是 16 的整数倍*。
</p>

<p>
  PS:我这里碰到这个问题,有可能是因为使用的 FFMpeg 库被修改过,用 SSE4 加速了 rgb 到 yuv420 的转换操作。原版的 FFMpeg 在这些奇怪的分辨率下是否会是如此,我没有验证。
</p></p>