一个h264视频的渲染问题的解决过程

2023-03-25  本文已影响0人  FingerStyle

前段时间在学音视频的过程中想用ffmpeg解码h264,然后通过opengles 来渲染,于是找了些网上的资料参考,实现了视频的解码和渲染。
解码部分:
参考ffplay ,通过demuxer和decoder 两个对象实现视频的解封装和解码,两个对象有各自的线程,确保性能不受影响

音视频同步:
视频向音频靠齐,通过对比 pts ,实现方法为

渲染部分:
由于测试的视频都是使用yuv420编码,因此使用了Y和UV双平面编码的方式,将解码后的数据打包到CVPixelBufferRef对象中,传递给openglES进行渲染。也可以直接传递数据给openglES,但不能使用CVOpenGLESTextureCacheCreateTextureFromImage这个方法,可以参考注释掉的代码使用glTexImage2D来载入纹理数据。
代码已经上传到github : https://github.com/fingerplay/FFMpegDecodeDemo,有需要的可以下载下来看看

问题

在测试过程中发现有个别视频显示的不对,例如resource.bundle里面的sintel.mov,显示如下:

IMG_0444.PNG
视频只有上半部分是正常的,下半部分全是绿色,但是用ffplay却可以正常显示。如果如果按下面的代码把pixelBuffer的高度改为原来的一半,倒是显示正常了, 这是为什么呢?
[attributes setObject:[NSNumber numberWithInt:frame->height/2] forKey: (NSString*)kCVPixelBufferHeightKey];

我对比了我自己的代码和 ffplay的源码,发现有两个不同之处:

  1. 我的代码直接使用了 openGL ES进行渲染,而ffplay里面使用了SDL作为渲染引擎,会不会SDL里面做了什么特殊的处理呢?
  2. 我的代码用了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), 得到了下面的图像

IMG_0447.PNG

这效果看起来还不如之前的,我不禁再次思考是不是这个视频的数据本身就有问题,只是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),果然图像正常显示了

IMG_0448.PNG

结论:

知识扩展

关于linesize 为什么会大于width, 可以看这篇文章,linesize是如何计算的
https://www.jianshu.com/p/aaef3631a802

上一篇 下一篇

猜你喜欢

热点阅读