前段时间在学音视频的过程中想用ffmpeg解码h264,然后通过opengles 来渲染,于是找了些网上的资料参考,实现了视频的解码和渲染。
解码部分:
参考ffplay ,通过demuxer和decoder 两个对象实现视频的解封装和解码,两个对象有各自的线程,确保性能不受影响
音视频同步:
视频向音频靠齐,通过对比 pts ,实现方法为
- (BOOL)scheduleVideoFrame:(AVFrame*)avFrame fps:(double)fps frame_delay:(double)frame_delay
渲染部分:
由于测试的视频都是使用yuv420编码,因此使用了Y和UV双平面编码的方式,将解码后的数据打包到CVPixelBufferRef对象中,传递给openglES进行渲染。也可以直接传递数据给openglES,但不能使用CVOpenGLESTextureCacheCreateTextureFromImage这个方法,可以参考注释掉的代码使用glTexImage2D来载入纹理数据。
代码已经上传到github : https://github.com/fingerplay/FFMpegDecodeDemo,有需要的可以下载下来看看
问题
在测试过程中发现有个别视频显示的不对,例如resource.bundle里面的sintel.mov,显示如下:
视频只有上半部分是正常的,下半部分全是绿色,但是用ffplay却可以正常显示。如果如果按下面的代码把pixelBuffer的高度改为原来的一半,倒是显示正常了, 这是为什么呢?
[attributes setObject:[NSNumber numberWithInt:frame->height/2] forKey: (NSString*)kCVPixelBufferHeightKey];
我对比了我自己的代码和 ffplay的源码,发现有两个不同之处:
- 我的代码直接使用了 openGL ES进行渲染,而ffplay里面使用了SDL作为渲染引擎,会不会SDL里面做了什么特殊的处理呢?
- 我的代码用了CVPixelBufferRef对解码后的数据进行包装,而ffplay 是直接传递数据给 SDL,会不会是从AVFrame到CVPixelBufferRef的过程中出现了数据丢失呢?
于是我便尝试接入SDL来渲染,发现确实能正常显示。
接入的代码见https://github.com/fingerplay/FFMpegDecodeDemo/commit/d709e14599acd35143649daed2458952c4731325
这就证明了SDL里面确实是有一些特殊处理 ,于是我又仔细研究SDL的源码, 发现它里面也是使用的openGLES,只不过对YUV的渲染不是使用双平面而是三平面,也就是Y、U、V各自绘制一次,具体代码可以看GLES2_CreateTexture和GLES2_UpdateTextureYUV这个方法
GLES2_CreateTexture(SDL_Renderer *renderer, SDL_Texture *texture)
static int
GLES2_UpdateTextureYUV(SDL_Renderer * renderer, SDL_Texture * texture,
const SDL_Rect * rect,
const Uint8 *Yplane, int Ypitch,
const Uint8 *Uplane, int Upitch,
const Uint8 *Vplane, int Vpitch)
然后我改成了三平面渲染(https://github.com/fingerplay/FFMpegDecodeDemo/commit/efb90950508cfdd3b4cef44ed2a4d4ace74fe039), 得到了下面的图像
这效果看起来还不如之前的,我不禁再次思考是不是这个视频的数据本身就有问题,只是SDL对其进行了纠错。
我仔细对比了我的代码和SDL的代码,发现SDL的方法会多传YUV三个通道的数据长度这些参数,而我的代码并没有用到这些,会不会是数据长度的问题呢?我又对比了一下AVFrame的数据,发现正常的图像解码出来 Y通道的lineSize和 图像的宽度是一样的,而有问题的图像Y通道的lineSize比图像的宽度大了几个字节,会不会正好就是多处的这几个字节导致了openGL渲染时下一行的数据错位呢? 带着疑问我又查看了SDL跟渲染相关的另外几个方法,发现在GLES2_TexSubImage2D这个方法里,对比了lineSize和图像宽度,如果两者相等就直接使用传进来的数据,也就是frame->data,如果不想等,则截取data前面width个字节
static int
GLES2_TexSubImage2D(GLES2_DriverContext *data, GLenum target, GLint xoffset, GLint yoffset, GLsizei width, GLsizei height, GLenum format, GLenum type, const GLvoid *pixels, GLint pitch, GLint bpp)
{
Uint8 *blob = NULL;
Uint8 *src;
int src_pitch;
int y;
/* Reformat the texture data into a tightly packed array */
src_pitch = width * bpp;
src = (Uint8 *)pixels;
if (pitch != src_pitch) {
blob = (Uint8 *)SDL_malloc(src_pitch * height);
if (!blob) {
return SDL_OutOfMemory();
}
src = blob;
for (y = 0; y < height; ++y)
{
SDL_memcpy(src, pixels, src_pitch);
src += src_pitch;
pixels = (Uint8 *)pixels + pitch;
}
src = blob;
}
data->glTexSubImage2D(target, 0, xoffset, yoffset, width, height, format, type, src);
if (blob) {
SDL_free(blob);
}
return 0;
}
而通过对源码的断点调试,也确实证明了我的猜想,SDL对 sintel.mov这个视频的每一帧图像的每一行数据都进行了截断,并把多余的字节移到了下一行。于是我仿照SDL的代码,写了一个类似的方法对AVFrame的数据进行修复再传给openGLES(https://github.com/fingerplay/FFMpegDecodeDemo/commit/c826398c0ed2784aec5fe70ccef0ff83ca9072a4),果然图像正常显示了
结论:
- 由于linesize>width,导致图像数据错位,而无法显示,可以对实际显示的大小对数据进行调整。
- 而使用CVPixelBufferRef 进行包装的数据,猜测是因为行对齐的原因,多余的数据被忽略,从而导致视频高度只有原来的一半。
知识扩展
关于linesize 为什么会大于width, 可以看这篇文章,linesize是如何计算的
https://www.jianshu.com/p/aaef3631a802