从AVPlayerItemVideoOutput到OpenGL纹理的最佳路径

8

我一直在苦恼如何从AVFoundation视频转换成OpenGL纹理,大部分的资料都和iOS相关,而且在OSX系统中无法正常工作。

首先,这是我设置videoOutput的方法:

NSDictionary *pbOptions = [NSDictionary dictionaryWithObjectsAndKeys:
                          [NSNumber numberWithInt:kCVPixelFormatType_422YpCbCr8], kCVPixelBufferPixelFormatTypeKey,
                          [NSDictionary dictionary], kCVPixelBufferIOSurfacePropertiesKey,
                                   nil];
        self.playeroutput   = [[AVPlayerItemVideoOutput alloc] initWithPixelBufferAttributes:pbOptions];
        self.playeroutput.suppressesPlayerRendering = YES;

我将尝试三种不同的解决方案,其中只有一种似乎能够始终正常工作,但我不确定它是否是最快的。其中一种可以在一段时间内工作,然后会出现框架跳动的问题,而另一种则只会产生黑色。
首先,使用glTexImage2D的有效解决方案。
- (BOOL) renderWithCVPixelBufferForTime: (NSTimeInterval) time
{
    CMTime vTime = [self.playeroutput itemTimeForHostTime:CACurrentMediaTime()];

    if ([self.playeroutput hasNewPixelBufferForItemTime:vTime]) {
        if (_cvPixelBufferRef) {
            CVPixelBufferUnlockBaseAddress(_cvPixelBufferRef, kCVPixelBufferLock_ReadOnly);
            CVPixelBufferRelease(_cvPixelBufferRef);
        }
        _cvPixelBufferRef = [self.playeroutput copyPixelBufferForItemTime:vTime itemTimeForDisplay:NULL];

        CVPixelBufferLockBaseAddress(_cvPixelBufferRef, kCVPixelBufferLock_ReadOnly);
        GLsizei       texWidth    = CVPixelBufferGetWidth(_cvPixelBufferRef);
        GLsizei       texHeight   = CVPixelBufferGetHeight(_cvPixelBufferRef);
        GLvoid *baseAddress = CVPixelBufferGetBaseAddress(_cvPixelBufferRef);


        glBindTexture(GL_TEXTURE_RECTANGLE_ARB, self.textureName);
        glTexParameteri(GL_TEXTURE_RECTANGLE_ARB, GL_TEXTURE_STORAGE_HINT_APPLE , GL_STORAGE_CACHED_APPLE);
        glTexImage2D(GL_TEXTURE_RECTANGLE_ARB, 0, GL_RGB, texWidth, texHeight, 0, GL_YCBCR_422_APPLE, GL_UNSIGNED_SHORT_8_8_APPLE, baseAddress);

        glBindTexture(GL_TEXTURE_RECTANGLE_ARB, 0);
    } 
    return YES;
} 

这种方法大部分时间都在锁定像素缓冲区的基地址,但文档说如果从GPU访问数据,则不需要锁定,否则可能会影响性能。我无法找到一种不用锁定就可以获取纹理的方法。

接下来是几乎可行的解决方案,使用iOSurface,这个方法一开始可以工作,但随后出现了严重的故障,好像之前帧中使用了ioSurfaces:

- (BOOL) renderWithIOSurfaceForTime:(NSTimeInterval) time {
    CMTime vTime = [self.playeroutput itemTimeForHostTime:CACurrentMediaTime()];

    if ([self.playeroutput hasNewPixelBufferForItemTime:vTime]) {

         CVPixelBufferRef pb = [self.playeroutput copyPixelBufferForItemTime:vTime itemTimeForDisplay:NULL];
         IOSurfaceRef newSurface = CVPixelBufferGetIOSurface(pb);
         if (_surfaceRef != newSurface) {
            IOSurfaceDecrementUseCount(_surfaceRef);
            _surfaceRef = newSurface;
            IOSurfaceIncrementUseCount(_surfaceRef);
            GLsizei       texWidth = (int) IOSurfaceGetWidth(_surfaceRef);
            GLsizei       texHeight= (int) IOSurfaceGetHeight(_surfaceRef);
            size_t        rowbytes = CVPixelBufferGetBytesPerRow(_cvPixelBufferRef);

            glBindTexture(GL_TEXTURE_RECTANGLE_ARB, self.textureName);
            CGLTexImageIOSurface2D(cgl_ctx, GL_TEXTURE_RECTANGLE_ARB, GL_RGB8, texWidth, texHeight, GL_YCBCR_422_APPLE, GL_UNSIGNED_SHORT_8_8_APPLE, _surfaceRef, 0);
            glBindTexture(GL_TEXTURE_RECTANGLE_ARB, 0);
        }
        CVPixelBufferRelease(pb);   
    }
    return YES;
}

如果能够奏效,这似乎是最佳解决方案。我有另一个从ioSurfaces创建纹理的过程,它工作得非常好,而且速度也非常快。

最后,在iOS上似乎推荐使用CVOpenGLTextureCache,osx上的实现略有不同,我无法使其渲染除黑色以外的任何内容,而且它似乎比第一种解决方案还要慢....

- (BOOL) renderByCVOpenGLTextureCacheForTime:(NSTimeInterval) time
{
    CMTime vTime = [self.playeroutput itemTimeForHostTime:CACurrentMediaTime()];

    if ([self.playeroutput hasNewPixelBufferForItemTime:vTime]) {
        _cvPixelBufferRef = [self.playeroutput copyPixelBufferForItemTime:vTime itemTimeForDisplay:NULL];
        if (!_textureCacheRef) {
            CVReturn error = CVOpenGLTextureCacheCreate(kCFAllocatorDefault, NULL, cgl_ctx, CGLGetPixelFormat(cgl_ctx), NULL, &_textureCacheRef);
            if (error) {
                NSLog(@"Texture cache create failed");
            }
        }

        CVReturn error = CVOpenGLTextureCacheCreateTextureFromImage(kCFAllocatorDefault, _textureCacheRef, _cvPixelBufferRef, NULL, &_textureRef);
        if (error) {
            NSLog(@"Failed to copy video texture");
        }


        CVPixelBufferRelease(_cvPixelBufferRef);

        _textureName = CVOpenGLTextureGetName(_textureRef);

    }
    return YES;
}

也许我没有正确设置东西,在osx中纹理缓存没有任何文档。

在渲染周期之间保留cvpixelbufferref是最好的,因为据我了解,纹理上传可以与CGLTexImage2d异步运行,我非常满意这一点,几个其他对象可以同时呈现,当纹理最终被绘制时,最终会调用cglflushDrawable。

我发现大多数苹果示例关于视频到OpenGL纹理都涉及iOS,并将纹理分成两部分以在着色器中重新组合,就像在这个例子中 https://developer.apple.com/library/ios/samplecode/GLCameraRipple/Listings/GLCameraRipple_main_m.html#//apple_ref/doc/uid/DTS40011222-GLCameraRipple_main_m-DontLinkElementID_11 我无法直接适应代码,因为iOS中的纹理缓存具有不同的实现。

所以任何指针都很好,它似乎是至关重要的功能,但我发现有关av foundation和opengl在osx上的信息非常消极。

更新:使用计数更新ioSurface代码,工作时间稍长,但最终仍会出现故障。

3个回答

5

我也像你一样正在学习OpenGL,对于它的知识了解得并不多,但是我注意到你的pbOptions中没有包含kCVPixelBufferOpenGLCompatibilityKey

NSDictionary *pbOptions = [NSDictionary dictionaryWithObjectsAndKeys:
    [NSNumber numberWithInt:kCVPixelFormatType_422YpCbCr8], kCVPixelBufferPixelFormatTypeKey,
    [NSDictionary dictionary], kCVPixelBufferIOSurfacePropertiesKey,
    [NSNumber numberWithBool:YES], kCVPixelBufferOpenGLCompatibilityKey, nil];

我将像素缓冲区请求为kCVPixelFormatType_32BGRA,而不是组件,这对于我的本地变量_currentSurface(IOSurfaceRef)、textureName(GLuint)、_sourceWidth(int)和_sourceHeight(int)有效。

IOSurfaceRef newSurface = CVPixelBufferGetIOSurface(pixelBuffer);
if (_currentSurface != newSurface) {
    CGLContextObj  cgl_ctx = (CGLContextObj)[[self openGLContext] CGLContextObj];
    [[self openGLContext] makeCurrentContext];

    IOSurfaceDecrementUseCount(_currentSurface);
    _currentSurface = newSurface;
    IOSurfaceIncrementUseCount(_currentSurface);
    GLsizei texWidth = (int) IOSurfaceGetWidth(_currentSurface);
    GLsizei texHeight = (int) IOSurfaceGetHeight(_currentSurface);

    if (_sourceWidth == 0 && _sourceHeight == 0) {
        // used during drawing of texture
        _sourceWidth = texWidth;
        _sourceHeight = texHeight;
    }

    if (!_textureName) {
        GLuint name;
        glGenTextures(1, &name);
        _textureName = name;
    }

    glBindTexture(GL_TEXTURE_RECTANGLE_ARB, _textureName);
    CGLTexImageIOSurface2D(cgl_ctx, GL_TEXTURE_RECTANGLE_ARB, GL_RGBA, texWidth, texHeight, GL_BGRA, GL_UNSIGNED_INT_8_8_8_8_REV, self.currentSurface, 0);        
    glBindTexture(GL_TEXTURE_RECTANGLE_ARB, 0);
}

谢谢,我已经尝试了,但是在上面的例子中没有找到任何立即的好处。在下一周,我会回来整理我的视频代码并进行一些尝试。 - George Brown
刚看到你的编辑,我很快会尝试你的建议,谢谢! - George Brown
3
你为什么对养羊这么了解? - braden
1
@jjxtra 从iOS 11开始,IOSurface现在是一个公共API。 - CIFilter
@LucasTizma,这是个好消息! - jjxtra
显示剩余2条评论

1
看起来,在OSX上快速视频纹理的对应方法是使用BiPlanar IOSurfaces,其中surfacePlane [0]是亮度(Y),surfacePlane [1]是下采样的色度(UV)。我的代码在Core Profile上运行,所以CGLTexImageIOSurface2D的GLenum常量反映了相同的内容。很确定只支持矩形纹理。我使用GLSL着色器将它们组合起来,在Sierra上运行得非常好。简而言之:
NSDictionary* pixelBufferAttributesIOSurfaceBiPlanar = [NSDictionary dictionaryWithObjectsAndKeys:
[NSNumber numberWithUnsignedInt:kPixelFormatTypeGL], ARC_BRIDGE(id)kCVPixelBufferPixelFormatTypeKey,
[NSNumber numberWithBool:YES], ARC_BRIDGE(id)kCVPixelBufferOpenGLCompatibilityKey,
[NSNumber numberWithBool:YES], ARC_BRIDGE(id)kCVPixelBufferIOSurfaceOpenGLTextureCompatibilityKey,
[NSDictionary dictionary], ARC_BRIDGE(id)kCVPixelBufferIOSurfacePropertiesKey,
nil];

/* luma texture */
CGLTexImageIOSurface2D(glContext,GL_TEXTURE_RECTANGLE,GL_R8,(GLsizei)textureSize.width,(GLsizei)textureSize.height,GL_RED,GL_UNSIGNED_BYTE,surfaceRef,0);
/* chroma texture */
CGLTexImageIOSurface2D(glContext,GL_TEXTURE_RECTANGLE,GL_RG8,(GLsizei)textureSize.width,(GLsizei)textureSize.height,GL_RG,GL_UNSIGNED_BYTE,surfaceRef,1);

GLSL

uniform sampler2DRect textureSampler0;
uniform sampler2DRect textureSampler1;
// ...
vec3 YCrCb;
vec2 lumaCoords = texCoord0;
vec2 chromaCoords = lumaCoords*vec2(0.5,0.5);
vec2 chroma = texture(textureSampler1,chromaCoords).xy;
float luma = texture(textureSampler0,lumaCoords).x;
YCrCb.x = (luma-(16.0/255.0)); // video range
YCrCb.yz = (chroma-vec2(0.5,0.5));
vec4 rgbA = vec4(colorConversionMatrix * YCrCb,1.0);

颜色转换矩阵应从CVPixelBufferRef生成。
CFTypeRef colorAttachments = CVBufferGetAttachment(pixelBuffer, kCVImageBufferYCbCrMatrixKey, NULL);
IOSurfaceRef是一个派生自CFTypeRef的对象,我使用CFRetain / CFRelease来保持对表面的控制,直到我不再需要纹理。如果CGLTexImageIOSurface2D执行GPU blit以上传纹理数据,则可能只需要在调用CGLTexImageIOSurface2D时使用CFRetain / Release。
我从苹果iOS示例代码AVBasicVideoOutput开始。我为iOS和OSX都移植了它,并花了一周时间尝试弄清OpenGL Core Profile版本。如果可以,请使用Metal:它更快,而且代码在iOS / OSX上几乎完全相同。此外,在WWDC13会议507中有一些很好的信息“OS X中的OpenGL新功能”。特别是,有一个GLSL着色器兼容性标志,允许EAGL着色器在OSX上运行大部分未修改。

0
当使用选项#2时,您是否将EAGLLayer的“保留背景”属性设置为NO?这可能是为什么它在不同周期中似乎使用相同的帧。看到您如何配置图层以适当地呈现帧缓冲区,或者是否配置了该图层对于您正在渲染帧缓冲区的视图会很有益...

你正在使用图层;它们是任何UIView或GLKView的重要组成部分。此外,你的纹理是通过上下文渲染到一个图层上的。你没有做的是配置视图的图层,这个视图的上下文渲染到该视图的图层上。 - James Bush
这原本是一个OSX的问题,我实际上是在我的NSOpenGLView子类中进行渲染,尽管它可能通过共享上下文在FBO中经过额外的处理。我使用非常相似的代码来捕获显示并将其呈现到完全相同的视图上,并且那样运行得很顺畅(但视频仍然会出现故障)。 - George Brown
值得注意的是,在iOS 11之前,IOSurface只能通过指定一些参数间接地在AV Foundation和Core Image(或许还有其他框架)中使用。在iOS 11中,它们似乎已经完全暴露了实际的IOSurface框架,这为跨媒体框架像素缓冲处理提供了更多有趣的可能性。 - CIFilter
有哪些可能性? - James Bush
AV Foundation、Core Image、Metal、GLES 等之间的互操作性。这些各种框架都有新的 API 可以传递 IOSurface 引用,因此这应该会打开更多直接缓冲区处理的路径,而无需复制或浪费时间/内存传输像素数据从一个框架到另一个框架。 - CIFilter
显示剩余4条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接