内容简介:以数据采集分为视频和音频:编码部分:
以 LFLiveSession
为中心切分成3部分:
- 前面是音视频的数据采集
- 后面是音视频数据推送到服务器
- 中间是音视频数据的编码
数据采集分为视频和音频:
- 视频由相机和一系列的滤镜组成,最后输出到预览界面(preview)和
LFLiveSession
- 音频使用
AudioUnit
读取音频,输出到LFLiveSession
编码部分:
- 视频提供软编码和硬编码,硬编码使用VideoToolBox。编码h264
- 音频提供AudioToolBox的硬编码,编码AAC
推送部分:
- 编码后的音视频按帧装入队列,循环推送
- 容器采用FLV,按照FLV的数据格式组装
- 使用librtmp库进行推送。
视频采集
视频采集部分内容比较多,可以分为几点:
- 相机
- 滤镜
- 链式图像处理方案
- opengl es
核心类,也是承担控制器角色的是 LFVideoCapture
,负责组装相机和滤镜,管理视频数据流。
1. 相机
相机的核心类是 GPUImageVideoCamera
AVFoundation
的
AVCaptureSession
,所以就是常规性的几步:
- 构建
AVCaptureSession
:_captureSession = [[AVCaptureSession alloc] init];
- 配置输入和输出,输入是设备,一般就有前后摄像头的区别
NSArray *devices = [AVCaptureDevice devicesWithMediaType:AVMediaTypeVideo]; for (AVCaptureDevice *device in devices) { if ([device position] == cameraPosition) { _inputCamera = device; } } ..... NSError *error = nil; videoInput = [[AVCaptureDeviceInput alloc] initWithDevice:_inputCamera error:&error]; if ([_captureSession canAddInput:videoInput]) { [_captureSession addInput:videoInput]; } 复制代码
- 输出可以是文件也可以是数据,这里因为要推送到服务器,而且也为了后续的图像处理,显然要用数据输出。
videoOutput = [[AVCaptureVideoDataOutput alloc] init]; [videoOutput setAlwaysDiscardsLateVideoFrames:NO]; ...... [videoOutput setSampleBufferDelegate:self queue:cameraProcessingQueue]; if ([_captureSession canAddOutput:videoOutput]) { [_captureSession addOutput:videoOutput]; } 复制代码
中间还一大段 captureAsYUV
为YES时执行的代码,有两种方式,一个是相机输出YUV格式,然后转成RGBA,还一种是直接输出BGRA,然后转成RGBA。前一种对应的是 kCVPixelFormatType_420YpCbCr8BiPlanarFullRange
或 kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange
,后一种对应的是 kCVPixelFormatType_32BGRA
,相机数据输出格式只接受这3种。中间的这一段的目的就是设置相机输出YUV,然后再转成RGBA。OpenGL和滤镜的问题先略过。
这里有个问题:h264编码时用的是YUV格式的,这里输出RGB然后又转回YUV不是浪费吗?还有输出YUV,然后自己转成RGB,然后编码时再转成YUV不是傻?如果直接把输出的YUV转码推送会怎么样?考虑到滤镜的使用,滤镜方便处理YUV格式的图像吗? 这些问题以后再深入研究,先看默认的流程里的处理原理。
配置完session以及输入输出,开启session后,数据从设备采集,然后调用dataOutput的委托方法: captureOutput:didOutputSampleBuffer:fromConnection
这里还有针对audio的处理,但音频不是在这采集的,这里的audio没启用,可以直接忽略先。
然后到方法 processVideoSampleBuffer:
,代码不少,干的就一件事:把相机输出的视频数据转到RGBA的格式的texture里。然后调用 updateTargetsForVideoCameraUsingCacheTextureAtWidth
这个方法把处理完的数据传递给下一个图像处理组件。
整体而言,相机就是收集设备的视频数据,然后倒入到图像处理链里。所以要搞清楚视频输出怎么传递到预览界面和 LFLiveSession
的,需要先搞清楚滤镜/图像处理链是怎么传递数据的。
2. 图像处理链
这里有两种处理组件: GPUImageOutput
和 GPUImageInput
。
GPUImageOutput
有一个target的概念的东西,在它处理完一个图像后,把图像传递给它的target。而 GPUImageInput
怎么接受从其他对象那传递过来的图像。通过这两个组件,就可以把一个图像从一个组件传递另一个组件,形成链条。有点像接水管?-_-
而且可以是交叉性的,如图:
有些滤镜是需要多个输入源,比如水印效果、蒙版效果,就可能出现D+E --->F的情况。这样的结构好处就是每个环节可以自由的处理自己的任务,而不需要管数据从哪来,要推到那里去。有数据它就处理,处理完就推到自己的tagets里去。
我比较好奇的是为什么 GPUImageOutput
定义成了类,而 GPUImageInput
却是协议,这也是值得思考的问题。
有了这两个组件的认识,再去到 LFVideoCapture
的 reloadFilter
方法。在这里,它把视频采集的处理链组装起来了,在这可以很清晰的看到图像数据的流动路线。
相机组件 GPUImageVideoCamera
继承于 GPUImageOutput
,它会把数据输出到它的target.
//< 480*640 比例为4:3 强制转换为16:9 if([self.configuration.avSessionPreset isEqualToString:AVCaptureSessionPreset640x480]){ CGRect cropRect = self.configuration.landscape ? CGRectMake(0, 0.125, 1, 0.75) : CGRectMake(0.125, 0, 0.75, 1); self.cropfilter = [[GPUImageCropFilter alloc] initWithCropRegion:cropRect]; [self.videoCamera addTarget:self.cropfilter]; [self.cropfilter addTarget:self.filter]; }else{ [self.videoCamera addTarget:self.filter]; } 复制代码
如果是640x480的分辨率,则路线是:videoCamera --> cropfilter --> filter,否则是videoCamera --> filter。
其他部分类似,就是条件判断是否加入某个组件,最后都会输出到: self.gpuImageView
和 self.output
。形成数据流大概:
self.gpuImageView
是视频预览图的内容视图,设置preview的代码:
- (void)setPreView:(UIView *)preView { if (self.gpuImageView.superview) [self.gpuImageView removeFromSuperview]; [preView insertSubview:self.gpuImageView atIndex:0]; self.gpuImageView.frame = CGRectMake(0, 0, preView.frame.size.width, preView.frame.size.height); } 复制代码
有了这个就可以看到经过一系列处理的视频图像了,这个是给拍摄者自己看到。
self.output
本身没什么内容,只是作为最后一个节点,把内容往外界传递出去:
__weak typeof(self) _self = self; [self.output setFrameProcessingCompletionBlock:^(GPUImageOutput *output, CMTime time) { [_self processVideo:output]; }]; ...... - (void)processVideo:(GPUImageOutput *)output { __weak typeof(self) _self = self; @autoreleasepool { GPUImageFramebuffer *imageFramebuffer = output.framebufferForOutput; CVPixelBufferRef pixelBuffer = [imageFramebuffer pixelBuffer]; if (pixelBuffer && _self.delegate && [_self.delegate respondsToSelector:@selector(captureOutput:pixelBuffer:)]) { [_self.delegate captureOutput:_self pixelBuffer:pixelBuffer]; } } } 复制代码
self.delegate
就是 LFLiveSession
对象,视频数据就流到了session部分,进入编码阶段。
3. 滤镜和OpenGL
滤镜的实现部分,先看一个简单的例子: GPUImageCropFilter
。在上面也用到了,就是用来做裁剪的。
它继承于 GPUImageFilter
,而 GPUImageFilter
继承于 GPUImageOutput <GPUImageInput>
,它既是一个output也是input。
作为input,会接收处理的图像,看 GPUImageVideoCamera
的 updateTargetsForVideoCameraUsingCacheTextureAtWidth
方法可以知道,传递给input的方法有两个:
-
setInputFramebuffer:atIndex
: 这个是传递GPUImageFramebuffer
对象 -
newFrameReadyAtTime:atIndex:
这个才是开启下一环节的处理。
GPUImageFramebuffer
是LFLiveKit封装的数据,用来在图像处理组件之间传递,包含了图像的大小、纹理、纹理类型、采样格式等。在图像处理链里传递图像,肯定需要一个统一的类型,除了图像本身,肯定还需要关于图像的信息,这样每个组件可以按相同的标准对待图像。 GPUImageFramebuffer
就起的这个作用。
GPUImageFramebuffer
内部核心的东西是 GLuint framebuffer
,即OpenGL里的frameBufferObject(FBO).关于FBO我也不是很了解,只知道它像一个容器,可以挂载了render buffer、texture、depth buffer等,也就是原本渲染输出到屏幕的东西,可以输出到一个FBO,然后可以拿这个渲染的结果进行再一次的处理。
在这个项目里,就是在FBO上挂载纹理,一次图像处理就是经历一次OpenGL渲染,处理前的图像用纹理的形式传入OpenGL,经历渲染流程输出到FBO, 图像数据就输出到FBO绑定的纹理上了。这样做了一次处理后数据结构还是一样,即绑定texture的FBO,可以再作为输入源提供给下一个组件。
FBO的构建具体看 GPUImageFramebuffer
的方法 generateFramebuffer
。
这里有一个值得学习的是 GPUImageFramebuffer
使用了一个缓存池,核心类 GPUImageFramebufferCache
。从流程里可以看得出 GPUImageFramebuffer
它是一个中间量,从组件A传递给组件B之后,B会使用这个framebuffer,B调用framebuffer的 lock
,使用完之后调用 unlock
。跟OC内存管理里的引用计数原理类似, lock
引用计数+1, unlock
-1,引用计数小于1就回归缓存池。需要一个新的frameBuffer的时候从优先从缓存池里拿,没有才构建。这一点又跟tableView的cell重用机制有点像。
缓冲区在数据流相关的程序是一个常用的功能,这种方案值得学习一下
说完 GPUImageFramebuffer
,再回到 newFrameReadyAtTime:atIndex
方法。
它里面就两个方法: renderToTextureWithVertices
这个是执行OpenGL ES的渲染操作, informTargetsAboutNewFrameAtTime
是通知它的target,把图像传递给下一环节处理。
上面的这些都是 GPUImageFilter
这个基类的,再回到 GPUImageCropFilter
这个裁剪功能的滤镜里。
它的贡献是根据裁剪区域的不同,提供了不同的 textureCoordinates
,这个是纹理坐标。它的init方法里使用的shader是 kGPUImageCropFragmentShaderString
,核心也就一句话: gl_FragColor = texture2D(inputImageTexture, textureCoordinate);
,使用纹理坐标采样纹理。所以对于输出结果而言, textureCoordinates
就是关键因素。
剪切和旋转效果都是通过修改纹理坐标的方式来达到的,vertext shader和fragment shader很简单,就是绘制一个矩形,然后使用纹理贴图
4. 纹理坐标的计算
我本以为剪切效果很简单,但是摸索到纹理坐标后发现是个巨坑,不是一两句解释的清,必须画图 -_-
顶点数据是:
static const GLfloat cropSquareVertices[] = { -1.0f, -1.0f, 1.0f, -1.0f, -1.0f, 1.0f, 1.0f, 1.0f, }; 复制代码
只有4个顶点,因为绘制矩形时使用的是 GL_TRIANGLE_STRIP
图元, 关于这个图元规则看这里 。
OpenGL的坐标是y向上,x向右,配合顶点数据可知4个角的索引是这个样子的:
纹理坐标跟OpenGL坐标方向是一样的的:
但是图像坐标却是跟它们反的,一个图片的数据是从左上角开始显示的,跟UI的坐标是一样的。也就是,读取一张图片作为texture后,纹理坐标(0, 0)读到的数据时图片左下角的。之前我搞晕了是:认为纹理坐标和OpenGL坐标是颠倒的,而没有意识到纹理和图像的区别。当用图片和用纹理做输入源时就有区别了。
有了3种坐标的认识,分析剪切效果的纹理坐标前还要先看下preview( GPUImageView
)的纹理坐标逻辑, 因为你眼睛看到的是preview的处理结果,它并不等于corpFiter的结果 ,不搞清它可能就被欺骗了。
-
蓝色的是图像/UI的坐标方向,橙色的是texture的坐标方向,绿色的是OpenGL的坐标方向。
-
相机后置摄像头默认输出
landScapeRight
方向的视频数据,这是麻烦的起源,虽然现在可以通过AVCaptureConnection
的videoOrientation
属性修改了。图里就是以这种情景为例子分析。 -
landScapeRight
就是逆时针旋转了,底边转到了右边。所以就有了图2。 -
然后图像和texture是上下颠倒的,所以有了图3。
-
然后分析3种处理情况,左转、右转和不旋转,就有了图4、5、6。
-
有个关键点是:preview是按上下颠倒的方式显示它接收的texture,因为:
- 视频采集结束后把数据输出给外界还是得通过图像的格式,这样其他播放器就可以不依赖于你的格式逻辑,都按照图像来处理。
- 希望传递给外界的图像是正确的,那么图像处理链结束输出的texture格式就是颠倒的。因为图像和texture坐标是上下颠倒的。
- preview它作为处理链输出接受者之一,接受的texture也就是颠倒的。这就造成了preview的纹理坐标是上下颠倒取的,这样显示出来才是对的。
- 所以在没有旋转的时候,preview的纹理坐标是:
static const GLfloat noRotationTextureCoordinates[] = { 0.0f, 1.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, }; 复制代码
结合顶点坐标数据,第1个顶点为(-1,-1)在左下角,纹理坐标是(0,1),在左上角。第3个顶点(-1, 1)在左上角,纹理坐标(0, 0),在左下角。
-
所以对于上图里的情景,正确显示应该取向右旋转的操作,即图5。这样显示出来,上下颠倒正好是图1。
-
所以如果不旋转,而是直接显示相机输出的图像,也就是接受图3的纹理,显示出来的样式就是图2。修改
GPUImageVideoCamera
的updateOrientationSendToTargets
方法,让outputRotation
为kGPUImageNoRotation
,就可以看到视频是旋转了90度的。当然事实是,我是眼睛看到了这个结果,再反推了里面的这些逻辑的。
以纹理/图像的角度看流程是这样:
蓝色是图像,红色是纹理。
就因为上面的原因,你眼睛看到的和纹理本身是上下相反的。直接显示相机输出的时候是 landscapeRight
,要想变竖直,看起来应该是向左转。但这个是图像显示左转,那么就是纹理坐标按右转的取。 说了那么多,坑在这里,图像的左转效果需要纹理的右转效果来实现 。
switch(_outputImageOrientation) { case UIInterfaceOrientationPortrait:outputRotation = kGPUImageRotateRight; break; case UIInterfaceOrientationPortraitUpsideDown:outputRotation = kGPUImageRotateLeft; break; ...... } 复制代码
cropFilter的纹理坐标计算
在回到剪切效果,虚线是剪切的位置:
计算使用的数据:
CGFloat minX = _cropRegion.origin.x; CGFloat minY = _cropRegion.origin.y; CGFloat maxX = CGRectGetMaxX(_cropRegion); CGFloat maxY = CGRectGetMaxY(_cropRegion); 复制代码
就是剪切区域的上下左右边界,看剪切+右转的情形。图6是最终期望的结果,但剪切是图像处理之一,它的输出是texture,所以它的输出是图3。第1个顶点,也就是左下角(-1, -1),对应的内容位置是1附近的虚线框顶点,1在输入的texture里是左上角,纹理坐标的x是距离边1-2的距离,纹理坐标y是距离距离边2-3的距离。
minX、minY这些数据是在哪个图的?图6。因为我们传入的数据是根据自己眼睛看到的样子来的,这个才是最终人需要的结果:
- minX是虚线框边1-4距离外框边1-4的距离
- minY是虚线框边1-2距离外框边1-2的距离
- maxX是虚线框边2-3距离外框边1-4的距离
- maxY是虚线框边4-3距离外框边1-2的距离
所以左下角的纹理坐标应该是(minY, 1-minX)。
以上所述就是小编给大家介绍的《视频库LFLiveKit分析》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:- 【视频课】数据分析快速入门
- Docker 镜像分析工具 Dive(附视频)
- 基于深度学习分析与检索海量短视频内容
- Python网络爬虫与文本数据分析(视频课)
- 视频演讲: Chatbots 中对话式交互系统的分析与应用
- 大规模机器学习在爱奇艺视频分析理解中的实践
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
High Performance Python
Micha Gorelick、Ian Ozsvald / O'Reilly Media / 2014-9-10 / USD 39.99
If you're an experienced Python programmer, High Performance Python will guide you through the various routes of code optimization. You'll learn how to use smarter algorithms and leverage peripheral t......一起来看看 《High Performance Python》 这本书的介绍吧!