筆記-GPUImage(三)短視頻錄制實時濾鏡以及濾鏡的切換

短視頻實時濾鏡(GPUImageVideoCamera)

demo下載地址:https://github.com/SXDgit/ZB_GPUImageVideoCamera

先看效果圖:

image

直接上代碼,后面解釋:

- (void)createVideoCamera {
    // 創建畫面捕獲器
    self.videoCamera = [[GPUImageVideoCamera alloc]initWithSessionPreset:AVCaptureSessionPreset640x480 cameraPosition:AVCaptureDevicePositionBack];
    // 輸出方向為豎屏
    self.videoCamera.outputImageOrientation = UIInterfaceOrientationPortrait;
    self.videoCamera.horizontallyMirrorRearFacingCamera = NO;
    self.videoCamera.horizontallyMirrorFrontFacingCamera = NO;
    self.videoCamera.runBenchmark = YES;
    
    // 構建組合濾鏡
    [self addGPUImageFilter:self.sepiaFilter];
    [self addGPUImageFilter:self.monochromeFilter];
    
    // 創建畫面呈現控件
    self.filterView = [[GPUImageView alloc]initWithFrame:self.view.frame];
    self.filterView.fillMode = kGPUImageFillModePreserveAspectRatio;
    self.view = self.filterView;
    
    [self.videoCamera addTarget:self.filterView];
    // 相機運行
    [self.videoCamera startCameraCapture];
    [self configMovie];
}

- (void)configMovie {
    // 設置寫入地址
    self.pathToMovie = [NSHomeDirectory() stringByAppendingPathComponent:[NSString stringWithFormat:@"Documents/ZBMovied%u.mp4", arc4random() % 1000]];
    // movieUrl指視頻寫入的地址
    self.moviewURL = [NSURL fileURLWithPath:_pathToMovie];
    self.movieWriter = [[GPUImageMovieWriter alloc]initWithMovieURL:_moviewURL size:CGSizeMake(480.0, 640.0)];
    _movieWriter.encodingLiveVideo = YES;
    // 設置聲音
    _videoCamera.audioEncodingTarget = _movieWriter;
}

- (void)addGPUImageFilter:(GPUImageFilter *)filter {
    [self.filterGroup addFilter:filter];
    
    GPUImageOutput<GPUImageInput> *newTerminalFilter = filter;
    NSInteger count = self.filterGroup.filterCount;
    if (count == 1) {
        self.filterGroup.initialFilters = @[newTerminalFilter];
        self.filterGroup.terminalFilter = newTerminalFilter;
    }else {
        GPUImageOutput<GPUImageInput> *terminalFilter = self.filterGroup.terminalFilter;
        NSArray *initialFilters = self.filterGroup.initialFilters;
        [terminalFilter addTarget:newTerminalFilter];
        self.filterGroup.initialFilters = @[initialFilters[0]];
        self.filterGroup.terminalFilter = newTerminalFilter;
    }
}

- (void)switchButtonAction {
    // 切換攝像頭前后翻轉
    [self.videoCamera rotateCamera];
    self.switchButton.selected = !self.switchButton.selected;
}

GPUImageFilter是用來接收源圖像,通過自定義的頂點、片元著色器來渲染新的圖像,并在繪制完成后通知響應鏈的下一個對象。
GPUImageVideoCamera提供來自攝像頭的圖像數據作為源數據,是GPUImageOutput的子類,一般是響應鏈的源頭。
GPUImageView一般用于顯示GPUImage的圖像,是響應鏈的終點。
GPUImageFilterGroup是多個filter的集合,terminalFilter為最終的filter,initialFilter是filter數組。本身不繪制圖像,對它的添加刪除Target操作,都會轉為terminalFilter的操作。

image

GPUImageMovieWriter是和GPUImageView處于同一地位的,都是視頻輸出類,只不過一個是輸出到文件,一個輸出到屏幕。
GPUImageBeautifyFilter基于GPUImage的實時美顏濾鏡中的美顏濾鏡,來自琨君。它是繼承于GPUImageFilterGroup。包括了GPUImageBilateralFilterGPUImageCannyEdgeDetectionFilterGPUImageCombinationFilterGPUImageHSBFilter

繪制流程

來自GPUImage詳細解析(三)- 實時美顏濾鏡

image

  • 1、GPUImageVideoCamera捕獲攝像頭圖像調用newFrameReadyAtTime: atIndex:通知GPUImageBeautifyFilter
  • 2、GPUImageBeautifyFilter調用newFrameReadyAtTime: atIndex:通知GPUImageBilateralFliter輸入紋理已經準備好;
  • 3、GPUImageBilateralFliter 繪制圖像后在informTargetsAboutNewFrameAtTime(),調用setInputFramebufferForTarget: atIndex:把繪制的圖像設置為GPUImageCombinationFilter輸入紋理,并通知GPUImageCombinationFilter紋理已經繪制完畢;
  • 4、GPUImageBeautifyFilter調用newFrameReadyAtTime: atIndex:通知 GPUImageCannyEdgeDetectionFilter輸入紋理已經準備好;
  • 5、同3,GPUImageCannyEdgeDetectionFilter 繪制圖像后,把圖像設置為GPUImageCombinationFilter輸入紋理;
  • 6、GPUImageBeautifyFilter調用newFrameReadyAtTime: atIndex:通知 GPUImageCombinationFilter輸入紋理已經準備好;
  • 7、GPUImageCombinationFilter判斷是否有三個紋理,三個紋理都已經準備好后調用GPUImageThreeInputFilter的繪制函數renderToTextureWithVertices: textureCoordinates:,圖像繪制完后,把圖像設置為GPUImageHSBFilter的輸入紋理,通知GPUImageHSBFilter紋理已經繪制完畢;
  • 8、GPUImageHSBFilter調用renderToTextureWithVertices: textureCoordinates:繪制圖像,完成后把圖像設置為GPUImageView的輸入紋理,并通知GPUImageView輸入紋理已經繪制完畢;
  • 9、GPUImageView把輸入紋理繪制到自己的幀緩存,然后通過[self.context presentRenderbuffer:GL_RENDERBUFFER]顯示到UIView上。

核心思路

通過GPUImageVideoCamera采集音視頻的信息,音頻信息直接發送給GPUImageMovieWriter,視頻信息傳入響應鏈作為源頭,渲染后的視頻信息再寫入GPUImageMovieWriter,同時GPUImageView顯示再屏幕上。

image

通過源碼可以知道GPUImage是使用AVFoundation框架來獲取視頻的。
AVCaptureSession類從AV輸入設備的采集數據到制定的輸出。
為了實現實時的圖像捕獲,要實現AVCaptureSession類,添加合適的輸入(AVCaptureDeviceInput)和輸出(比如AVCaptureMovieFileOutput)調用startRunning開始輸入到輸出的數據流,調用stopRunning停止數據流。需要注意的是startingRunning函數會花費一定的時間,所以不能在主線程調用,防止卡頓。

image

流程解析:
1、找到物理設備攝像頭_inputCamera、麥克風_microphone,創建攝像頭輸入videoInput和麥克風輸入audioInput
2、設置videoInputaudioInput_captureSession的輸入,同時設置videoOutputaudioOutput_captureSession的輸出,并且設置videoOutputaudioOutput的輸出delegate
3、_captureSession調用startRunning,開始捕獲信號。
4、音頻數據到達,把數據轉發給之前設置的audioEncodingTarget,并通過調用assetWriterAudioInputappendSampleBuffer方法寫入音頻數據。
5、視頻數據到達,視頻數據傳入響應鏈,經過處理后通過assetWriterPixelBufferInputappendSampleBuffer方法寫入視頻數據。
6、視頻錄制完成,保存寫入手機相冊。

踩過的坑

1、錄制后保存在相冊里的視頻是白屏?
在初始化movieWriter的過程中,使用addTarget:增加了濾鏡導致。

2、錄制完視頻后,再次點擊錄制,會crash?
報錯的原因是[AVAssetWriter startWriting] Cannot call method when status is 3,報錯是在[self.movieWriter startRecording];這行代碼,追溯源碼,可以看到GPUImageMovieWriter是對AssetWriter進行了一次封裝,其核心的寫文件還是由AssetWriter完成。
通過源碼可以發現[self.movieWriter finishRecording];以后并沒有新建一個AssetWriter實例。所以可以保存視頻到相冊成功后,加入下面幾行代碼:

- (void)videoCameraReset {
    [_videoCamera removeTarget:_movieWriter];
    [[NSFileManager defaultManager] removeItemAtURL:_moviewURL error:nil];
    [self initMovieWriter];
    [_videoCamera addTarget:_movieWriter];
}

- (void)initMovieWriter {
    _movieWriter = [[GPUImageMovieWriter alloc]initWithMovieURL:_moviewURL size:CGSizeMake(480.0, 640.0)];
    _movieWriter.encodingLiveVideo = YES;
}

1、攝像頭實例取消對GPUImageMovieWriter的綁定,因為重新實例化新的GPUImageMovieWriter以后原來的實例就沒用了。
2、刪除原來已經寫好的影片文件,如果新的實例直接寫入已存在的文件會報錯AVAssetWriterStatusFailed
3、重新實例化一個GPUImageMovieWriter
4、把新的GPUImageMovieWriter綁定到攝像頭實例。

這樣以后就可以不同的錄制保存了。參考[紹棠] GPUImageMovieWriter 無法2次錄像 報錯:[AVAssetWriter startWriting] Cannot call method when status is 3

參考資料:落影大佬的GPUImage文集

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,702評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,143評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,553評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,620評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,416評論 6 405
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,940評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,024評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,170評論 0 287
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,709評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,597評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,784評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,291評論 5 357
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,029評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,407評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,663評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,403評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,746評論 2 370