第八章 OpenGL层
使用核心动画的OpenGL层,CAOpenGLLayer使你在播放电影时可以做更多的控制,这些控制包括在视频帧中使用核心图像滤镜或者组合混合视频流到同一个上下文中。
这一章展示给你,如何用CAOpenGLLayer来渲染一个视频通道,就像第七章QuickTime层的功能一样。下面,我们就来看,利用CAOpenGLLayer,在网格层上来组合多种视频通道,就像你可以在视频墙上看到的一样。这里演示了,当你使用CAOpenGLLayer层时,你可以控制那些功能和效果。
在CAOpenGLLayer上渲染视频
就像第七章讨论的,简单的视频播放可以用QTMovieView和QTMovieLayer来控制。然而,假如你在渲染之前想改变目前的帧,你最好使用OpenGl。第一步,我们来看在CAOpenGlLayer上,如何简单的展示没有改变的帧。这里模拟了第七章在QTMovieLayer上已经实现的功能。
为了利用CAOpenGLLayer,你需要子类化它,它不可以直接使用的。相比于你在核心动画中需要做的东西,用CAOpenGLLayer层创建OpenGL的内容要显的简单的多。在一个NSOpenGLView中,你需要设置每件事,但是在CAOpenGLLayer中,下面的都可以免费得到。
预先配置的OpenGL上下文。
视频端口自动设定到CAOpenGLLayer帧上
预先配置的像素格式对象
设置这些代码如此简单,以至于你需要关心仅仅2个功能。第一个功能核对下一帧是否要被渲染,第二个函数就会根据第一个函数是否返回YES或者NO,来决定要不要渲染到内容上。如果你很好的理解了这两个功能函数,你也就很好的理解了CAOpenGLLayer如何工作了,重要的时如何来使用它。这俩函数如表8-1所示。
- -(BOOL)canDrawInCGLContext:(CGLContextObj)glContextpixelFormat:(CGLPixelFormatObj)pixelFormat
- forLayerTime:(CFTimeInterval)timeIntervaldisplayTime:(const CVTimeStamp *)timestamp;
- -(void)drawInCGLContext:(CGLContextObj)glContextpixelFormat:(CGLPixelFormatObj)pixelFormat
- forLayerTime:(CFTimeInterval)intervaldisplayTime:(const CVTimeStamp *)timestamp;
表8-1 CAOpenGLLayer的代理渲染函数
只有当你设定了层的同步属性asynchronous为YES的时候,函数-canDrawInCGLContext才会被调用。你可以在你的继承CAOpenGLLayer的层的初始化方法init中,调用此方法:
[selfsetAsynchronous:YES];
如果你计划手动的更新内容或者根据定时器进行安排,那么你就不需要设定这些。在这种情况下,无论什么时候想要刷新内容,只需要简单的调用-setNeedsDisplay:YES这个函数就行了。
对于我们的情况,然而,我们想要-canDrawInCGLContext被调用,因为当电影播放时,我们需要不停的核对已经准备好的帧。为了获取这些帧,需要设定asynchronous属性为YES。
只有当-canDrawInCGLContext返回YES的时候,函数-drawInCGLContext会被调用。在它被调用之后,就可以渲染你的OpenGL内容到需要的上下文中。
层的时序
注意到-canDrawInCGLContext和-drawInCGLContext,这两个关系时间的字段。
forLayerTime,属于CGTimeInterval
displayTime,属于CVTimeStamp
我们不关心forLayerTime,因为我们在CAOpenGLLayer层上,不需要要使用它。然而,displayTime对于我们这个练习至关重要。
根据显示的刷新率,正确的播放视频,并且同步音频可能有些微妙的关系。然而,苹果公司有了一个稳定的播放视频方式,这里他们用到了显示链接(display link)。这里有苹果公司在核心视频程序向导中定义的显示链接:
为了简单的同步视频和显示的刷新率,核心视频提供了一个特别的定时器叫做显示链接。显示链接在一个独立的高优先级的线程中运行,这个线程不会被应用程序的交互处理影响到。
过去,同步视频帧和显示的刷新率是一个问题,尤其是如果你也有音频时。很简单的你就可以想到,例如通过定时器输出一个帧时,但是这不可能考虑到用户交互,CPU装载,窗口组合等等这些问题的时间延时。核心视频中的展示链接,基于展示的类型和延时,做了一个非常智能的评估,从而可以获得什么时候一个帧需要输出。
实质上,也就是说你必须要创建一个回调函数,它会被定期的调用,在这个回调函数中,你可以核对一个新的帧在此刻(也就是回调函数中的inOutputTime这个CVTimeStamp这个时刻点)是否可用。显示链接的回调如清单8-2.
- CVReturnMyDisplayLinkCallback ( CVDisplayLinkRef displayLink,
- const CVTimeStamp *inNow,
- const CVTimeStamp*inOutputTime, CVOptionFlags flagsIn, CVOptionFlags *flagsOut,
- void *displayLinkContext);
清单8-2
然而,在CAOpenGLLayer中安装和使用显示链接的回调函数是完全没有必要的,因为这些功能是通过-canDrawInCGLContext和-drawInCGLContext这两个函数实现的。你就可以核对在函数参数中displayTime这个时刻是否一个新的帧是被提供。
因此,通过-canDrawInCGLContext这个函数,来渲染一个视频帧到CAOpenGLLayer上的步骤:
1.核对视频是否正在播放;如果没有返回no。
2.如果视频是在播放,核对是否视频的上下文已经被设定。如果没有,通过调用-setupVisualContext这个方法设定它。
3.检查是否一个新的帧是准备好了。
4.如果可以,复制目前的图像到图像缓存中。
5.如果上述每个都是成功的,返回YES
6.如果-canDrawInCGLContext返回了YES,-drawInCGLContext会被调用。在这里面就用绘制OpenGL的线条到视频目前的纹理图像上。
-canDrawInCGLContext在清单8-3中的实现。
- -(BOOL)canDrawInCGLContext:(CGLContextObj)glContextpixelFormat:(CGLPixelFormatObj)pixelFormat
- forLayerTime:(CFTimeInterval)timeIntervaldisplayTime:(const CVTimeStamp *)timeStamp
- {
- if( !qtVisualContext ) {
- // If the visual context forthe QTMovie has not been set up
- // we initialize it now
- [selfsetupVisualContext:glContext withPixelFormat:pixelFormat];
- }
- // Check to see if a newframe (image) is ready to be drawn at // the current time by passing NULL asthe second param if(QTVisualContextIsNewImageAvailable(qtVisualContext,NULL))
- {
- // Release the previous frame
- CVOpenGLTextureRelease(currentFrame);
- // Copy the current frameinto the image buffer
- QTVisualContextCopyImageForTime(qtVisualContext,NULL,
- NULL, ¤tFrame);
- // Returns the texturecoordinates for the
- // part of the image thatshould be displayed CVOpenGLTextureGetCleanTexCoords(currentFrame,
- lowerLeft, lowerRight,upperRight, upperLeft);
- return YES; }
- return NO; }
清单 8-3 –canDrawInCGLContext代理的实现
在你要在一个OpenGL的上下文中画任何东西,你必须先给QuickTime视频安装可视的上下文。-setupVisualContext的代码如清单8-4.
- -(void)setupVisualContext:(CGLContextObj)glContextwithPixelFormat:(CGLPixelFormatObj)pixelFormat;
- {
- OSStatus error;
- NSDictionary *attributes =nil;
- attributes = [NSDictionarydictionaryWithObjectsAndKeys:
- [NSDictionarydictionaryWithObjectsAndKeys:
- [NSNumbernumberWithFloat:[self frame].size.width],kQTVisualContextTargetDimensions_WidthKey,
- [NSNumbernumberWithFloat:[self frame].size.height],kQTVisualContextTargetDimensions_HeightKey, nil],
- kQTVisualContextTargetDimensionsKey,[NSDictionary dictionaryWithObjectsAndKeys:
- [NSNumber numberWithFloat:[selfframe].size.width], kCVPixelBufferWidthKey,
- [NSNumbernumberWithFloat:[self frame].size.height], kCVPixelBufferHeightKey, nil],
- kQTVisualContextPixelBufferAttributesKey,nil];
- // Create the QuickTimevisual context
- error = QTOpenGLTextureContextCreate(NULL,glContext,
- pixelFormat,(CFDictionaryRef)attributes, &qtVisualContext);
- // Associate it with themovie
- SetMovieVisualContext([moviequickTimeMovie],qtVisualContext); }
清单8-4 QuickTime的可视上下文的实现
在-setupVisualContext中,视频是联系CAOpenGLLayer中的OpenGL的上下文。这样,当-drawInCGLContext是被调用时,每件事都被设定好了,就可以调用OpenGL的API了,就像清单8-5所示。
- -(void)drawInCGLContext:(CGLContextObj)glContextpixelFormat:(CGLPixelFormatObj)pixelFormat
- forLayerTime:(CFTimeInterval)intervaldisplayTime:(const CVTimeStamp *)timeStamp
- {
- NSRect bounds =NSRectFromCGRect([self bounds]);
- GLfloat minX, minY, maxX,maxY;
- minX = NSMinX(bounds); minY =NSMinY(bounds); maxX = NSMaxX(bounds); maxY = NSMaxY(bounds);
- glMatrixMode(GL_MODELVIEW);glLoadIdentity(); glMatrixMode(GL_PROJECTION); glLoadIdentity();
- glOrtho( minX, maxX, minY,maxY, -1.0, 1.0); glClearColor(0.0, 0.0, 0.0, 0.0);
- glClear(GL_COLOR_BUFFER_BIT);
- CGRect imageRect = [selfframe];
- // Enable target for thecurrent frame glEnable(CVOpenGLTextureGetTarget(currentFrame));
- // Bind to the current frame
- // This tells OpenGL whichtexture we want
- // to draw so when we makethe glTexCord and
- // glVertex calls, thecurrent frame gets drawn
- // to the contextglBindTexture(CVOpenGLTextureGetTarget(currentFrame),
- CVOpenGLTextureGetName(currentFrame));glMatrixMode(GL_TEXTURE);
- glLoadIdentity();glColor4f(1.0, 1.0, 1.0, 1.0); glBegin(GL_QUADS);
- // Draw the quads
- glTexCoord2f(upperLeft[0],upperLeft[1]); glVertex2f (imageRect.origin.x,
- imageRect.origin.y +imageRect.size.height); glTexCoord2f(upperRight[0], upperRight[1]);
- glVertex2f(imageRect.origin.x + imageRect.size.width, imageRect.origin.y +imageRect.size.height);
- glTexCoord2f(lowerRight[0],lowerRight[1]);
- glVertex2f(imageRect.origin.x + imageRect.size.width,
- imageRect.origin.y);glTexCoord2f(lowerLeft[0], lowerLeft[1]);
- glVertex2f(imageRect.origin.x, imageRect.origin.y);
- glEnd();
- // This CAOpenGLLayer isresponsible to flush // the OpenGL context so we call super
- [superdrawInCGLContext:glContext
- pixelFormat:pixelFormatforLayerTime:interval
- displayTime:timeStamp]; //Task the context
- QTVisualContextTask(qtVisualContext);}
清单 8-5 drawInCGLContext的实现
要理解上面的代码,你可能需要理解OpenGL,这就超过了本书讲述的范围。然而,看清单8-5中这两行代码。
glEnable(CVOpenGLTextureGetTarget(currentFrame));glBindTexture(CVOpenGLTextureGetTarget(currentFrame),
CVOpenGLTextureGetName(currentFrame));
这两行告诉了OpenGL能够绑定绘图的纹理到目前的帧上(CVImageBufferRef),它是通过调用-canDrawInContext获得的。总之,这是告诉OpenGL要绘制什么东西。
渲染多个视频渠道
这一章最后的目标是来演示如何在一个CAOpenGLLayer层中,渲染多个QuickTime视频流。刚才提到了,使用OpenGL代替QTMovieLayer的原因归结于性能。当你使用多个QTMovieLayers导入和播放多个QTMovies时,性能会迅速的下降。为了提高性能,我们替代去获得播放视频的每个帧,而是组合他们一起到同一个OpenGL上下文中。
为了完成这个目标,我们的做法不同于先前段落中使用OpenGL来渲染一个单一的QuickTime视频。我们为每个QuickTime视频都创建了一个图像缓冲区,实时的来核对是否下一帧准备在-canDrawInCGLContext中被调用。我们也要通过在每一个动画初始化时为其设置一个可绘矩形的方法,来在网格中显示动画。
我们可以复制粘贴上面段落中我们写的代码,但是这样的话代码会变得笨重和冗余。因此,我们使用面向对象的方法,这里来创建一个继承自OpenGL层的对象(叫做OpenGLVidGridLayer),一个VideoChannel对象代表一个视频流,然后一个VideoChannelController对象提供了一个播放和渲染的接口。下面是每个对象要做的事情:
OpenGLVidGridLayer
这个对象用来初始化层以便同步运行,同时设置框架大小,设置背景颜色为黑色,设置传进来的视频路径的数组,初始化VideoChannel和VideoChannelController对象,调用-canDrawInCGContext和-drawInCGLContext方法,并且作为一个代理,来开始播放我们分配给的VideoChannel对象的视频。
VideoChannel
这个代表着每个方格中的视频。它存储了一些区域,这些区域将会在父区域中被使用来组合视频,并且要核对它分配的视频是否准备绘制下一个帧,这里还要使用初始化指定的高和宽来初始化可视区域,使用OpenGL的调用来绘制分配的视频,并且为视频的播放提供一个代理方法。
VideoChannelController
这个对象包含了VideoChannel对象的数组,这些对象都有一个初始化的视频和区域,然后用来在方格中渲染。它提供了一个代理函数,来指导所有的VideoChannel来播放和停止视频,并且还提供了一个代理函数来看是否所有的VideoChannel都是准备被绘制到下一个帧上,还提供了一个代理函数来告知所有的VideoChannel来设置它们的可视上下文,调用在VideoChannel中的OpenGL绘图的初始化代码,并且还提供了一个代理函数告知所有的VideoChannel来渲染它们的视频到各自的区域。
面向对象的方法是一把双刃剑。尽管它可以使我们用更清晰的方式组织东西,但是必须要用对象的思想考虑每件事,因此学习这个描述方法,以便于你知道工程中的那些代码代表什么在运行。VideoChannelController对象提供了每个VideoChannels对象的控制,所以看这个对象如何工作,是你理解代码的一个最佳的选择。OpenGLVideGridLayer提供了一些初始化的代码,并且提供了一个功能,决定我们是否要在当前的时间进行绘制。
表8-1描述了应用程序的例子,OpenGL VidGrid。窗口中所有展示的区域都是继承自CAOpenGLLayer这个类的一个子类,叫做OpenGLVidGridLayer。屏幕上展示的每个独立的分割的视频都是由VideoChannel这个类的代码来呈现的。
图 8-1 可视的对象
实现继承子CAOpenGLLayer的子类OpenGLVidGridLayer
继承自CAOpenGLLayer的类,OpenGLVidGridLayer提供了绘制所有视频通道到同一个OpenGL上下文中的入口。初始化代码接收了一个QTMovie对象的列表,同时为这些QTMovie对象计算展示的区域。它为每个QTMovies对象创建了一个VideoChannel对象,并且增加他们到一个数组中。VideoChannelController对象会被初始化,然后这个VideoChannels数组对象就分配给它。实现的代码如清单8-6.
- - (void)initVideoChannels; {