以LFLiveSession
爲中心切分紅3部分:緩存
數據採集分爲視頻和音頻:bash
LFLiveSession
AudioUnit
讀取音頻,輸出到LFLiveSession
編碼部分:服務器
推送部分:session
視頻採集部份內容比較多,能夠分爲幾點:數據結構
核心類,也是承擔控制器角色的是LFVideoCapture
,負責組裝相機和濾鏡,管理視頻數據流。架構
相機的核心類是GPUImageVideoCamera
ide
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格式的圖像嗎? 這些問題之後再深刻研究,先看默認的流程裏的處理原理。 更新:對於這個問題的一點猜想:硬件輸出的(包括攝像頭和硬件碼)的格式是yuv裏面的NV12,而通常經常使用的視頻裏yuv的顏色空間具體是yuv420,這兩種的區別只是uv數據是分紅兩層仍是交錯在一塊兒。但NV12的格式也是能夠直接用opengl(es)渲染的,因此仍是有疑問。ui
配置完session以及輸入輸出,開啓session後,數據從設備採集,而後調用dataOutput的委託方法:captureOutput:didOutputSampleBuffer:fromConnection
編碼
這裏還有針對audio的處理,但音頻不是在這採集的,這裏的audio沒啓用,能夠直接忽略先。
而後到方法processVideoSampleBuffer:
,代碼很多,乾的就一件事:把相機輸出的視頻數據轉到RGBA的格式的texture裏。而後調用updateTargetsForVideoCameraUsingCacheTextureAtWidth
這個方法把處理完的數據傳遞給下一個圖像處理組件。
總體而言,相機就是收集設備的視頻數據,而後倒入到圖像處理鏈裏。因此要搞清楚視頻輸出怎麼傳遞到預覽界面和LFLiveSession
的,須要先搞清楚濾鏡/圖像處理鏈是怎麼傳遞數據的。
這裏有兩種處理組件: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部分,進入編碼階段。
濾鏡的實現部分,先看一個簡單的例子: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很簡單,就是繪製一個矩形,而後使用紋理貼圖
我本覺得剪切效果很簡單,可是摸索到紋理座標後發現是個巨坑,不是一兩句解釋的清,必須畫圖 -_-
頂點數據是:
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種處理狀況,左轉、右轉和不旋轉,就有了圖四、五、6。
有個關鍵點是:preview是按上下顛倒的方式顯示它接收的texture,由於:
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;
......
}
複製代碼
在回到剪切效果,虛線是剪切的位置:
計算使用的數據:
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。由於咱們傳入的數據是根據本身眼睛看到的樣子來的,這個纔是最終人須要的結果:
因此左下角的紋理座標應該是(minY, 1-minX)。
花了不少的篇幅去說紋理座標的問題,一開始原本想挑個簡單例子(cropFiler)說下濾鏡組件的,可是這個紋理座標的計算讓我陷入了糊塗,不搞清楚實在不舒服。
更輕鬆的解決方案?
值得學習的地方: