視頻庫LFLiveKit分析

總體架構

LFLiveSession爲中心切分紅3部分:緩存

  • 前面是音視頻的數據採集
  • 後面是音視頻數據推送到服務器
  • 中間是音視頻數據的編碼

總體架構.png

數據採集分爲視頻和音頻:bash

  • 視頻由相機和一系列的濾鏡組成,最後輸出到預覽界面(preview)和LFLiveSession
  • 音頻使用AudioUnit讀取音頻,輸出到LFLiveSession

編碼部分:服務器

  • 視頻提供軟編碼和硬編碼,硬編碼使用VideoToolBox。編碼h264
  • 音頻提供AudioToolBox的硬編碼,編碼AAC

推送部分:session

  • 編碼後的音視頻按幀裝入隊列,循環推送
  • 容器採用FLV,按照FLV的數據格式組裝
  • 使用librtmp庫進行推送。

視頻採集

視頻採集部份內容比較多,能夠分爲幾點:數據結構

  • 相機
  • 濾鏡
  • 鏈式圖像處理方案
  • opengl es

核心類,也是承擔控制器角色的是LFVideoCapture,負責組裝相機和濾鏡,管理視頻數據流。架構


1. 相機

相機的核心類是GPUImageVideoCameraide

相機數據流程.png
視頻採集使用系統庫 AVFoundationAVCaptureSession,因此就是常規性的幾步:

  1. 構建AVCaptureSession:_captureSession = [[AVCaptureSession alloc] init];
  2. 配置輸入和輸出,輸入是設備,通常就有先後攝像頭的區別
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];
  }
複製代碼
  1. 輸出能夠是文件也能夠是數據,這裏由於要推送到服務器,並且也爲了後續的圖像處理,顯然要用數據輸出。
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_420YpCbCr8BiPlanarFullRangekCVPixelFormatType_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的,須要先搞清楚濾鏡/圖像處理鏈是怎麼傳遞數據的。


2. 圖像處理鏈

這裏有兩種處理組件:GPUImageOutputGPUImageInput

GPUImageOutput有一個target的概念的東西,在它處理完一個圖像後,把圖像傳遞給它的target。而GPUImageInput怎麼接受從其餘對象那傳遞過來的圖像。經過這兩個組件,就能夠把一個圖像從一個組件傳遞另外一個組件,造成鏈條。有點像接水管?-_-

並且能夠是交叉性的,如圖:

圖像處理鏈.png

有些濾鏡是須要多個輸入源,好比水印效果、蒙版效果,就可能出現D+E --->F的狀況。這樣的結構好處就是每一個環節能夠自由的處理本身的任務,而不須要管數據從哪來,要推到那裏去。有數據它就處理,處理完就推到本身的tagets裏去。

我比較好奇的是爲何GPUImageOutput定義成了類,而GPUImageInput倒是協議,這也是值得思考的問題。

有了這兩個組件的認識,再去到LFVideoCapturereloadFilter方法。在這裏,它把視頻採集的處理鏈組裝起來了,在這能夠很清晰的看到圖像數據的流動路線。

相機組件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.gpuImageViewself.output。造成數據流大概:

視頻採集基本數據流.png

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,會接收處理的圖像,看GPUImageVideoCameraupdateTargetsForVideoCameraUsingCacheTextureAtWidth方法能夠知道,傳遞給input的方法有兩個:

  • setInputFramebuffer:atIndex: 這個是傳遞GPUImageFramebuffer對象
  • newFrameReadyAtTime:atIndex: 這個纔是開啓下一環節的處理。

GPUImageFramebuffer是LFLiveKit封裝的數據,用來在圖像處理組件之間傳遞,包含了圖像的大小、紋理、紋理類型、採樣格式等。在圖像處理鏈裏傳遞圖像,確定須要一個統一的類型,除了圖像自己,確定還須要關於圖像的信息,這樣每一個組件能夠按相同的標準對待圖像。GPUImageFramebuffer就起的這個做用。

GPUImageFramebuffer內部核心的東西是GLuint framebuffer,即OpenGL裏的frameBufferObject(FBO).關於FBO我也不是很瞭解,只知道它像一個容器,能夠掛載了render buffer、texture、depth buffer等,也就是本來渲染輸出到屏幕的東西,能夠輸出到一個FBO,而後能夠拿這個渲染的結果進行再一次的處理。

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個角的索引是這個樣子的:

頂點位置.png

紋理座標跟OpenGL座標方向是同樣的的:

紋理座標.png

可是圖像座標倒是跟它們反的,一個圖片的數據是從左上角開始顯示的,跟UI的座標是同樣的。也就是,讀取一張圖片做爲texture後,紋理座標(0, 0)讀到的數據時圖片左下角的。以前我搞暈了是:認爲紋理座標和OpenGL座標是顛倒的,而沒有意識到紋理和圖像的區別。當用圖片和用紋理作輸入源時就有區別了。

有了3種座標的認識,分析剪切效果的紋理座標前還要先看下preview(GPUImageView)的紋理座標邏輯,由於你眼睛看到的是preview的處理結果,它並不等於corpFiter的結果,不搞清它可能就被欺騙了。

視頻圖像、紋理座標變換.png

  • 藍色的是圖像/UI的座標方向,橙色的是texture的座標方向,綠色的是OpenGL的座標方向。

  • 相機後置攝像頭默認輸出landScapeRight方向的視頻數據,這是麻煩的起源,雖然如今能夠經過AVCaptureConnectionvideoOrientation屬性修改了。圖裏就是以這種情景爲例子分析。

  • landScapeRight就是逆時針旋轉了,底邊轉到了右邊。因此就有了圖2。

  • 而後圖像和texture是上下顛倒的,因此有了圖3。

  • 而後分析3種處理狀況,左轉、右轉和不旋轉,就有了圖四、五、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。修改GPUImageVideoCameraupdateOrientationSendToTargets方法,讓outputRotationkGPUImageNoRotation,就能夠看到視頻是旋轉了90度的。固然事實是,我是眼睛看到了這個結果,再反推了裏面的這些邏輯的。

以紋理/圖像的角度看流程是這樣:

座標變換.png

藍色是圖像,紅色是紋理。

就由於上面的緣由,你眼睛看到的和紋理自己是上下相反的。直接顯示相機輸出的時候是landscapeRight,要想變豎直,看起來應該是向左轉。但這個是圖像顯示左轉,那麼就是紋理座標按右轉的取。說了那麼多,坑在這裏,圖像的左轉效果須要紋理的右轉效果來實現

switch(_outputImageOrientation)
{
    case UIInterfaceOrientationPortrait:outputRotation = kGPUImageRotateRight; break;
    case UIInterfaceOrientationPortraitUpsideDown:outputRotation = kGPUImageRotateLeft; break;
    ......
}
複製代碼
cropFilter的紋理座標計算

在回到剪切效果,虛線是剪切的位置:

紋理旋轉+剪切的邏輯示意圖.png

計算使用的數據:

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)。


最後

花了不少的篇幅去說紋理座標的問題,一開始原本想挑個簡單例子(cropFiler)說下濾鏡組件的,可是這個紋理座標的計算讓我陷入了糊塗,不搞清楚實在不舒服。

更輕鬆的解決方案?

  1. 把旋轉作成單獨的處理組件,不要和其餘的濾鏡混在一塊兒了,其餘處理組件就按照當前不旋轉的樣式來。
  2. 這些旋轉+剪切的邏輯可能一個矩陣運算就直接搞定了,那樣會更好理解些。

值得學習的地方:

  • 視頻圖像處理
  • 緩衝區/緩存重用機制
  • 鏈式圖像處理
  • 整個庫的封裝很好:從LFLiveSession,到LFVideoCapture+LFAudioCapture,到GPUImageVideoCamera,層次清晰,每層的存在恰到好處。
相關文章
相關標籤/搜索