先附上參考資料
http://www.jianshu.com/p/16cb14f53933
https://developer.apple.com/library/content/samplecode/AVSimpleEditoriOS/Introduction/Intro.html
https://github.com/objcio/VideoCaptureDemo
https://github.com/gsixxxx/DTSmallVideo
https://github.com/AndyFightting/VideoRecord
卷首吐槽語
這還是第一次接觸自定義介面錄製視訊,包括各種引數的設定,不得不說,錄製視訊這塊,各種類,各種方法,蠻複雜的,網上的資料也是各種雜亂,想要弄清楚還真是得費一番功夫,我參考了大量資料,根據自己的思路整理了一遍,按照我的思路來,保證你看一遍就會,我這裡只是簡單的錄製,壓縮,剪裁,匯出等功能,不設計濾鏡,新增背景音樂,合併,字幕等等,重要的是這個流程,主流程會了,其他也就是錦上添花了。
先附上dome demo地址
腦圖
方便大家對三中錄製方式有一個大概的瞭解,看一下這張圖片。 第一種採用系統的錄製較為簡單,詳細介紹後面兩種。
效果圖
demo中把三種方式單獨分開,便於學習。支援閃光燈,切換鏡頭,錄製不同尺寸的視訊等。
1.UIImagePickerController
這種方式只能設定一些簡單引數,自定義程度不高,只能自定義介面上的操作按鈕,還有視訊的畫質等。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 |
- (void)viewDidLoad { [super viewDidLoad]; if ([self isVideoRecordingAvailable]) { return; } self.sourceType = UIImagePickerControllerSourceTypeCamera; self.mediaTypes = @[(NSString *)kUTTypeMovie]; self.delegate = self; //隱藏系統自帶UI self.showsCameraControls = NO; //設定攝像頭 [self switchCameraIsFront:NO]; //設定視訊畫質類別 self.videoQuality = UIImagePickerControllerQualityTypeMedium; //設定散光燈型別 self.cameraFlashMode = UIImagePickerControllerCameraFlashModeAuto; //設定錄製的最大時長 self.videoMaximumDuration = 20; } - (BOOL)isVideoRecordingAvailable { if([UIImagePickerController isSourceTypeAvailable:UIImagePickerControllerSourceTypeCamera]){ NSArray *availableMediaTypes = [UIImagePickerController availableMediaTypesForSourceType:UIImagePickerControllerSourceTypeCamera]; if([availableMediaTypes containsObject:(NSString *)kUTTypeMovie]){ return YES; } } return NO; } - (void)switchCameraIsFront:(BOOL)front { if (front) { if([UIImagePickerController isCameraDeviceAvailable:UIImagePickerControllerCameraDeviceFront]){ [self setCameraDevice:UIImagePickerControllerCameraDeviceFront]; } } else { if([UIImagePickerController isCameraDeviceAvailable:UIImagePickerControllerCameraDeviceRear]){ [self setCameraDevice:UIImagePickerControllerCameraDeviceRear]; } } } |
2.AVCaptureSession+AVCaptureMovieFileOutput
流程:
1 2 3 4 5 6 7 |
1. 建立捕捉會話 2. 設定視訊的輸入 3. 設定音訊的輸入 4. 輸出源設定,這裡視訊,音訊資料會合併到一起輸出,在代理方法中國也可以單獨拿到視訊或者音訊資料,給AVCaptureMovieFileOutput指定路徑,開始錄製之後就會向這個路徑寫入資料 5. 新增視訊預覽層 6. 開始採集資料,這個時候還沒有寫入資料,使用者點選錄製後就可以開始寫入資料 |
0. 建立捕捉會話
1 2 3 4 5 |
self.session = [[AVCaptureSession alloc] init]; if ([_session canSetSessionPreset:AVCaptureSessionPreset640x480]) {//設定解析度 _session.sessionPreset=AVCaptureSessionPreset640x480; } |
1. 視訊的輸入
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
- (void)setUpVideo { // 1.1 獲取視訊輸入裝置(攝像頭) AVCaptureDevice *videoCaptureDevice=[self getCameraDeviceWithPosition:AVCaptureDevicePositionBack];//取得後置攝像頭 // 視訊 HDR (高動態範圍影像) // videoCaptureDevice.videoHDREnabled = YES; // 設定最大,最小幀速率 //videoCaptureDevice.activeVideoMinFrameDuration = CMTimeMake(1, 60); // 1.2 建立視訊輸入源 NSError *error=nil; self.videoInput= [[AVCaptureDeviceInput alloc] initWithDevice:videoCaptureDevice error:&error]; // 1.3 將視訊輸入源新增到會話 if ([self.session canAddInput:self.videoInput]) { [self.session addInput:self.videoInput]; } } |
2. 音訊的輸入
1 2 3 4 5 6 7 8 9 10 |
// 2.1 獲取音訊輸入裝置 AVCaptureDevice *audioCaptureDevice=[[AVCaptureDevice devicesWithMediaType:AVMediaTypeAudio] firstObject]; NSError *error=nil; // 2.2 建立音訊輸入源 self.audioInput = [[AVCaptureDeviceInput alloc] initWithDevice:audioCaptureDevice error:&error]; // 2.3 將音訊輸入源新增到會話 if ([self.session canAddInput:self.audioInput]) { [self.session addInput:self.audioInput]; } |
3.輸出源設定
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
- (void)setUpFileOut { // 3.1初始化裝置輸出物件,用於獲得輸出資料 self.FileOutput=[[AVCaptureMovieFileOutput alloc]init]; // 3.2設定輸出物件的一些屬性 AVCaptureConnection *captureConnection=[self.FileOutput connectionWithMediaType:AVMediaTypeVideo]; //設定防抖 //視訊防抖 是在 iOS 6 和 iPhone 4S 釋出時引入的功能。到了 iPhone 6,增加了更強勁和流暢的防抖模式,被稱為影院級的視訊防抖動。相關的 API 也有所改動 (目前為止並沒有在文件中反映出來,不過可以檢視標頭檔案)。防抖並不是在捕獲裝置上配置的,而是在 AVCaptureConnection 上設定。由於不是所有的裝置格式都支援全部的防抖模式,所以在實際應用中應事先確認具體的防抖模式是否支援: if ([captureConnection isVideoStabilizationSupported ]) { captureConnection.preferredVideoStabilizationMode=AVCaptureVideoStabilizationModeAuto; } //預覽圖層和視訊方向保持一致 captureConnection.videoOrientation = [self.previewlayer connection].videoOrientation; // 3.3將裝置輸出新增到會話中 if ([_session canAddOutput:_FileOutput]) { [_session addOutput:_FileOutput]; } } |
4. 視訊預覽層
一進入視訊錄製介面,這個時候 session就已經在採集資料了,並把資料顯示在預覽層上,使用者選擇錄製後,再將採集到的資料寫入檔案。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
- (void)setUpPreviewLayerWithType:(FMVideoViewType )type { CGRect rect = CGRectZero; switch (type) { case Type1X1: rect = CGRectMake(0, 0, kScreenWidth, kScreenWidth); break; case Type4X3: rect = CGRectMake(0, 0, kScreenWidth, kScreenWidth*4/3); break; case TypeFullScreen: rect = [UIScreen mainScreen].bounds; break; default: rect = [UIScreen mainScreen].bounds; break; } self.previewlayer.frame = rect; [_superView.layer insertSublayer:self.previewlayer atIndex:0]; } |
5. 開始採集畫面
1 2 |
[self.session startRunning]; |
6.開始錄製
1 2 3 4 5 6 7 8 |
- (void)writeDataTofile { NSString *videoPath = [self createVideoFilePath]; self.videoUrl = [NSURL fileURLWithPath:videoPath]; [self.FileOutput startRecordingToOutputFileURL:self.videoUrl recordingDelegate:self]; } |
3.AVCaptureSession+AVAssetWriter
流程:
1 2 3 4 5 6 7 |
1. 建立捕捉會話 2. 設定視訊的輸入 和 輸出 3. 設定音訊的輸入 和 輸出 4. 新增視訊預覽層 5. 開始採集資料,這個時候還沒有寫入資料,使用者點選錄製後就可以開始寫入資料 6. 初始化AVAssetWriter, 我們會拿到視訊和音訊的資料流,用AVAssetWriter寫入檔案,這一步需要我們自己實現。 |
1. 建立捕捉會話
需要確保在同一個佇列,最好佇列只建立一次
1 2 3 4 5 |
self.session = [[AVCaptureSession alloc] init]; if ([_session canSetSessionPreset:AVCaptureSessionPreset640x480]) {//設定解析度 _session.sessionPreset=AVCaptureSessionPreset640x480; } |
2.設定視訊的輸入 和 輸出
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
- (void)setUpVideo { // 2.1 獲取視訊輸入裝置(攝像頭) AVCaptureDevice *videoCaptureDevice=[self getCameraDeviceWithPosition:AVCaptureDevicePositionBack];//取得後置攝像頭 // 2.2 建立視訊輸入源 NSError *error=nil; self.videoInput= [[AVCaptureDeviceInput alloc] initWithDevice:videoCaptureDevice error:&error]; // 2.3 將視訊輸入源新增到會話 if ([self.session canAddInput:self.videoInput]) { [self.session addInput:self.videoInput]; } self.videoOutput = [[AVCaptureVideoDataOutput alloc] init]; self.videoOutput.alwaysDiscardsLateVideoFrames = YES; //立即丟棄舊幀,節省記憶體,預設YES [self.videoOutput setSampleBufferDelegate:self queue:self.videoQueue]; if ([self.session canAddOutput:self.videoOutput]) { [self.session addOutput:self.videoOutput]; } } |
3. 設定音訊的輸入 和 輸出
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
- (void)setUpAudio { // 2.2 獲取音訊輸入裝置 AVCaptureDevice *audioCaptureDevice=[[AVCaptureDevice devicesWithMediaType:AVMediaTypeAudio] firstObject]; NSError *error=nil; // 2.4 建立音訊輸入源 self.audioInput = [[AVCaptureDeviceInput alloc] initWithDevice:audioCaptureDevice error:&error]; // 2.6 將音訊輸入源新增到會話 if ([self.session canAddInput:self.audioInput]) { [self.session addInput:self.audioInput]; } self.audioOutput = [[AVCaptureAudioDataOutput alloc] init]; [self.audioOutput setSampleBufferDelegate:self queue:self.videoQueue]; if([self.session canAddOutput:self.audioOutput]) { [self.session addOutput:self.audioOutput]; } } |
4. 新增視訊預覽層
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
- (void)setUpPreviewLayerWithType:(FMVideoViewType )type { CGRect rect = CGRectZero; switch (type) { case Type1X1: rect = CGRectMake(0, 0, kScreenWidth, kScreenWidth); break; case Type4X3: rect = CGRectMake(0, 0, kScreenWidth, kScreenWidth*4/3); break; case TypeFullScreen: rect = [UIScreen mainScreen].bounds; break; default: rect = [UIScreen mainScreen].bounds; break; } self.previewlayer.frame = rect; [_superView.layer insertSublayer:self.previewlayer atIndex:0]; } |
5. 開始採集畫面
1 2 |
[self.session startRunning]; |
6. 初始化AVAssetWriter
AVAssetWriter 寫入資料的過程需要在子執行緒中執行,並且每次寫入資料都需要保證在同一個執行緒。
1 2 3 4 5 6 7 8 |
- (void)setUpWriter { self.videoUrl = [[NSURL alloc] initFileURLWithPath:[self createVideoFilePath]]; self.writeManager = [[AVAssetWriteManager alloc] initWithURL:self.videoUrl viewType:_viewType]; self.writeManager.delegate = self; } |
7.拿到資料流後處理
視訊資料和音訊資料需要分開處理
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
- (void)captureOutput:(AVCaptureOutput *)captureOutput didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection *)connection { @autoreleasepool { //視訊 if (connection == [self.videoOutput connectionWithMediaType:AVMediaTypeVideo]) { if (!self.writeManager.outputVideoFormatDescription) { @synchronized(self) { CMFormatDescriptionRef formatDescription = CMSampleBufferGetFormatDescription(sampleBuffer); self.writeManager.outputVideoFormatDescription = formatDescription; } } else { @synchronized(self) { if (self.writeManager.writeState == FMRecordStateRecording) { [self.writeManager appendSampleBuffer:sampleBuffer ofMediaType:AVMediaTypeVideo]; } } } } //音訊 if (connection == [self.audioOutput connectionWithMediaType:AVMediaTypeAudio]) { if (!self.writeManager.outputAudioFormatDescription) { @synchronized(self) { CMFormatDescriptionRef formatDescription = CMSampleBufferGetFormatDescription(sampleBuffer); self.writeManager.outputAudioFormatDescription = formatDescription; } } @synchronized(self) { if (self.writeManager.writeState == FMRecordStateRecording) { [self.writeManager appendSampleBuffer:sampleBuffer ofMediaType:AVMediaTypeAudio]; } } } } } |
我們拿到最原始的資料以後,可以對其進行各種引數的設定
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 |
- (void)setUpWriter { self.assetWriter = [AVAssetWriter assetWriterWithURL:self.videoUrl fileType:AVFileTypeMPEG4 error:nil]; //寫入視訊大小 NSInteger numPixels = self.outputSize.width * self.outputSize.height; //每畫素位元 CGFloat bitsPerPixel = 6.0; NSInteger bitsPerSecond = numPixels * bitsPerPixel; // 位元速率和幀率設定 NSDictionary *compressionProperties = @{ AVVideoAverageBitRateKey : @(bitsPerSecond), AVVideoExpectedSourceFrameRateKey : @(30), AVVideoMaxKeyFrameIntervalKey : @(30), AVVideoProfileLevelKey : AVVideoProfileLevelH264BaselineAutoLevel }; //視訊屬性 self.videoCompressionSettings = @{ AVVideoCodecKey : AVVideoCodecH264, AVVideoScalingModeKey : AVVideoScalingModeResizeAspectFill, AVVideoWidthKey : @(self.outputSize.height), AVVideoHeightKey : @(self.outputSize.width), AVVideoCompressionPropertiesKey : compressionProperties }; _assetWriterVideoInput = [AVAssetWriterInput assetWriterInputWithMediaType:AVMediaTypeVideo outputSettings:self.videoCompressionSettings]; //expectsMediaDataInRealTime 必須設為yes,需要從capture session 實時獲取資料 _assetWriterVideoInput.expectsMediaDataInRealTime = YES; _assetWriterVideoInput.transform = CGAffineTransformMakeRotation(M_PI / 2.0); // 音訊設定 self.audioCompressionSettings = @{ AVEncoderBitRatePerChannelKey : @(28000), AVFormatIDKey : @(kAudioFormatMPEG4AAC), AVNumberOfChannelsKey : @(1), AVSampleRateKey : @(22050) }; _assetWriterAudioInput = [AVAssetWriterInput assetWriterInputWithMediaType:AVMediaTypeAudio outputSettings:self.audioCompressionSettings]; _assetWriterAudioInput.expectsMediaDataInRealTime = YES; if ([_assetWriter canAddInput:_assetWriterVideoInput]) { [_assetWriter addInput:_assetWriterVideoInput]; }else { NSLog(@"AssetWriter videoInput append Failed"); } if ([_assetWriter canAddInput:_assetWriterAudioInput]) { [_assetWriter addInput:_assetWriterAudioInput]; }else { NSLog(@"AssetWriter audioInput Append Failed"); } self.writeState = FMRecordStateRecording; } |
設定好引數以後,就可以寫入檔案了。AVAssetWriter資料寫入的過程有點複雜,demo中我新建AVAssetWriteManager分離出AVAssetWriter,單獨處理寫資料,這樣邏輯會清晰一點。
fileOut和writer的相同點和不同點
從上面的兩個流程大致可以看出來, 相同點:資料採集都在AVCaptureSession中進行,視訊和音訊的輸入都一樣,畫面的預覽一致。 不同點: 輸出不一致, AVCaptureMovieFileOutput 只需要一個輸出即可,指定一個檔案路後,視訊和音訊會寫入到指定路徑,不需要其他複雜的操作。 AVAssetWriter 需要 AVCaptureVideoDataOutput 和 AVCaptureAudioDataOutput 兩個單獨的輸出,拿到各自的輸出資料後,然後自己進行相應的處理。
可配引數不一致,AVAssetWriter可以配置更多的引數。
視訊剪裁不一致,AVCaptureMovieFileOutput 如果要剪裁視訊,因為系統已經把資料寫到檔案中了,我們需要從檔案中獨到一個完整的視訊,然後處理;而AVAssetWriter我們拿到的是資料流,還沒有合成視訊,對資料流進行處理,所以兩則剪裁方式也是不一樣。
其他新增背景音樂,水印等也是不一樣的,這裡沒有涉及就不介紹了。到這裡也差不多了,文章也有點長了。這些是我自己整理資料總結出來的,不排除會有一些錯誤之處,供大家學習參考,希望有所收穫。如果方便,還請為我star一個,也算是對我的支援。
打賞支援我寫出更多好文章,謝謝!
打賞作者
打賞支援我寫出更多好文章,謝謝!
任選一種支付方式