短視頻實時濾鏡(GPUImageVideoCamera)
demo下載地址:https://github.com/SXDgit/ZB_GPUImageVideoCamera
先看效果圖:
直接上代碼,后面解釋:
- (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
的操作。
GPUImageMovieWriter
是和GPUImageView
處于同一地位的,都是視頻輸出類,只不過一個是輸出到文件,一個輸出到屏幕。GPUImageBeautifyFilter
是基于GPUImage的實時美顏濾鏡中的美顏濾鏡,來自琨君。它是繼承于GPUImageFilterGroup
。包括了GPUImageBilateralFilter
、GPUImageCannyEdgeDetectionFilter
、GPUImageCombinationFilter
、GPUImageHSBFilter
。
繪制流程
- 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
顯示再屏幕上。
通過源碼可以知道GPUImage
是使用AVFoundation
框架來獲取視頻的。
AVCaptureSession
類從AV輸入設備的采集數據到制定的輸出。
為了實現實時的圖像捕獲,要實現AVCaptureSession
類,添加合適的輸入(AVCaptureDeviceInput
)和輸出(比如AVCaptureMovieFileOutput
)調用startRunning
開始輸入到輸出的數據流,調用stopRunning
停止數據流。需要注意的是startingRunning
函數會花費一定的時間,所以不能在主線程調用,防止卡頓。
流程解析:
1、找到物理設備攝像頭
_inputCamera
、麥克風_microphone
,創建攝像頭輸入videoInput
和麥克風輸入audioInput
。2、設置
videoInput
和audioInput
為_captureSession
的輸入,同時設置videoOutput
和audioOutput
為_captureSession
的輸出,并且設置videoOutput
和audioOutput
的輸出delegate
。3、
_captureSession
調用startRunning
,開始捕獲信號。4、音頻數據到達,把數據轉發給之前設置的
audioEncodingTarget
,并通過調用assetWriterAudioInput
的appendSampleBuffer
方法寫入音頻數據。5、視頻數據到達,視頻數據傳入響應鏈,經過處理后通過
assetWriterPixelBufferInput
的appendSampleBuffer
方法寫入視頻數據。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文集