BSFramework 组件包:
- 2D、3D无限轮播图组件
- 图片视频选择、图片视频预览、图片视频拍摄组件
- GitHub 地址
- iOS 自定义相机 拍照+视频录制(一) 拍照篇
框架:
关键类: AVAssetWriter
大致需要属性
@property (nonatomic ,strong) AVAssetWriter *writer;//视频采集
@property (nonatomic ,strong) AVAssetWriterInput *writerAudioInput;//视频采集
@property (nonatomic ,strong) AVAssetWriterInput *writerVideoInput;//视频采集
大致流程:
- session 初始化
- device 初始化
- input,output 初始化,并关联device
- session 添加 input 和 output
- 初始化预览层 AVCaptureVideoPreviewLayer
- session startRunning
- 点击录制按钮, 录制视频 代理回调后,处理数据,写入文件,生成视频
PS : 和 拍照的区别点在于,首先,需要使用不同的 input 和 output ,视频拍摄还需要音频的输入输出设备,其次,回调的代理是不一样的,并且只要我们 session 执行了 startRunning 回调就会一直进行,所以需要一个额外的变量来控制什么时候开始真正的写入视频数据
视频的输入输出初始化
#pragma mark 设置视频类型的输入输出源
-(void)addVideoIO{
//视频输出源
NSDictionary *videoSetting = @{(id)kCVPixelBufferPixelFormatTypeKey:@(kCVPixelFormatType_32BGRA)};
self.videoPutData = [[AVCaptureVideoDataOutput alloc]init];
self.videoPutData.videoSettings = videoSetting;
dispatch_queue_t videoQueue = dispatch_queue_create("vidio", DISPATCH_QUEUE_CONCURRENT);
[self.videoPutData setSampleBufferDelegate:self queue:videoQueue];
if ([self.session canAddOutput:self.videoPutData]) {
[self.session addOutput:self.videoPutData];
}
//音频输入源
AVCaptureDevice *audioDevice = [[AVCaptureDevice devicesWithMediaType:AVMediaTypeAudio]firstObject];
NSError *audioError = nil;
self.audioInput = [[AVCaptureDeviceInput alloc]initWithDevice:audioDevice error:&audioError];
if (!audioError) {
if ([self.session canAddInput:self.audioInput]) {
[self.session addInput:self.audioInput];
}
}
//音频输出源
self.audioPutData = [[AVCaptureAudioDataOutput alloc]init];
if ([self.session canAddOutput:self.audioPutData]) {
[self.session addOutput:self.audioPutData];
}
dispatch_queue_t audioQueue = dispatch_queue_create("audio", DISPATCH_QUEUE_CONCURRENT);
[self.audioPutData setSampleBufferDelegate:self queue:audioQueue];
}
点击拍摄即初始化 writer
// 配置 AVAssetWriter
-(void)configWriter{
dispatch_queue_t writeQueueCreate = dispatch_queue_create("writeQueueCreate", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(writeQueueCreate, ^{
NSError *error = nil;
self.preVideoURL = [self getVideoURL];
self.writer = [AVAssetWriter assetWriterWithURL:self.preVideoURL fileType:AVFileTypeMPEG4 error:&error];
if (!error) {
NSInteger numPixels = SCREEN_WIDTH * SCREEN_HEIGHT;
//每像素比特
CGFloat bitsPerPixel = 12.0;
NSInteger bitsPerSecond = numPixels * bitsPerPixel;
// 码率和帧率设置
NSDictionary *compressionProperties = @{ AVVideoAverageBitRateKey : @(bitsPerSecond),
AVVideoExpectedSourceFrameRateKey : @(15),
AVVideoMaxKeyFrameIntervalKey : @(10),
AVVideoProfileLevelKey : AVVideoProfileLevelH264BaselineAutoLevel };
//视频属性
NSDictionary *videoSetting = @{ AVVideoCodecKey : AVVideoCodecH264,
AVVideoWidthKey : @(SCREEN_HEIGHT * 2),
AVVideoHeightKey : @(SCREEN_WIDTH * 2),
AVVideoScalingModeKey : AVVideoScalingModeResizeAspectFill,
AVVideoCompressionPropertiesKey : compressionProperties };
NSDictionary *audioSetting = @{ AVEncoderBitRatePerChannelKey : @(28000),
AVFormatIDKey : @(kAudioFormatMPEG4AAC),
AVNumberOfChannelsKey : @(1),
AVSampleRateKey : @(22050) };
self.writerAudioInput = [AVAssetWriterInput assetWriterInputWithMediaType:AVMediaTypeAudio outputSettings:audioSetting];
self.writerAudioInput.expectsMediaDataInRealTime = YES;
self.writerVideoInput = [AVAssetWriterInput assetWriterInputWithMediaType:AVMediaTypeVideo outputSettings:videoSetting];
self.writerVideoInput.expectsMediaDataInRealTime = YES;
self.writerVideoInput.transform = CGAffineTransformMakeRotation(M_PI/2.0);
if ([self.writer canAddInput:self.writerAudioInput]) {
[self.writer addInput:self.writerAudioInput];
}
if ([self.writer canAddInput:self.writerVideoInput]) {
[self.writer addInput:self.writerVideoInput];
}
self.videoRecording = YES;
}else{
NSLog(@"write 初始化失败:%@",error);
}
});
}
处理回调
//视频录制回调
-(void)captureOutput:(AVCaptureOutput *)output didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection *)connection{
if (!self.videoRecording) {
return;
}
CMFormatDescriptionRef desMedia = CMSampleBufferGetFormatDescription(sampleBuffer);
CMMediaType mediaType = CMFormatDescriptionGetMediaType(desMedia);
if (mediaType == kCMMediaType_Video) {
/**
* 要点1:
* 由于 self.canWritting = YES 放在 startSessionAtSourceTime
* 下,会出现录制的视频 前几帧相同(可能2-3帧都是第一帧) 的问题,故而将
* self.canWritting = YES 放在 startsession 上,目前测试没出现问题
*/
/**
* 要点2:
* 对于 startSessionAtSourceTime 开启时机需要放在类型为
* kCMMediaType_Video 里判断,因为如果放在外边,可能会导致录制的时候
* 是没有画面的,但是有声音,这就导致了预览视频的时候发现开头有一段空白视频
* 但是是有声音的
*/
/**
* 要点3:
* 需要将 startSessionAtSourceTime 方法放在类型为kCMMediaType_Video里
* 确保第一帧为图像在开启录制
*/
if (!self.canWritting) {
[self.writer startWriting];
CMTime timestamp = CMSampleBufferGetPresentationTimeStamp(sampleBuffer);
self.canWritting = YES;
[self.writer startSessionAtSourceTime:timestamp];
}
}
if (self.canWritting) {
if (mediaType == kCMMediaType_Video) {
if (self.writerVideoInput.readyForMoreMediaData ) {
BOOL success = [self.writerVideoInput appendSampleBuffer:sampleBuffer];
if (!success) {
NSLog(@"video write failed");
}
}
}else if (mediaType == kCMMediaType_Audio && self.canWritting){
if (self.writerAudioInput.readyForMoreMediaData) {
BOOL success = [self.writerAudioInput appendSampleBuffer:sampleBuffer];
if (!success) {
NSLog(@"audio write failed");
}
}
}
}
}
PS:处理视频回调有几个需要注意的点,都在代码的注释上写好了
视频预览
// 视频录制完成后 预览
-(void)previewVideo{
if (self.playerLayer) {
[self.playerLayer removeFromSuperlayer];
self.player = nil;
self.playerLayer = nil;
}
AVPlayerItem *playerItem = [AVPlayerItem playerItemWithAsset:[AVAsset assetWithURL:self.preVideoURL]];
self.player = [[AVPlayer alloc]initWithPlayerItem:playerItem];
self.playerLayer = [AVPlayerLayer playerLayerWithPlayer:self.player];
self.playerLayer.videoGravity = AVLayerVideoGravityResizeAspect;
self.playerLayer.frame = self.view.frame;
[self.view.layer addSublayer:self.playerLayer];
[self.player play];
[self.view bringSubviewToFront:self.bottomView];
}
存储视频
//videoPath为视频下载到本地之后的本地路径
- (void)saveVideoToAlbum:(NSString*)videoPath{
if(videoPath) {
BOOL compatible = UIVideoAtPathIsCompatibleWithSavedPhotosAlbum(videoPath);
if(compatible){
UISaveVideoAtPathToSavedPhotosAlbum(videoPath,self,@selector(video:didFinishSavingWithError:contextInfo:),nil);
}
}
}
//保存视频完成之后的回调
- (void)video:(NSString *)videoPath didFinishSavingWithError:(NSError *)error contextInfo:(void *)contextInfo{
if(error) {
NSLog(@"保存视频失败%@", error);
}else{
// 保存视频成功后退出,刷新相册,退出界面
if ([self.delegate respondsToSelector:@selector(photoCameraNextBtnClickedWithVideoPath:)]) {
[self.delegate photoCameraNextBtnClickedWithVideoPath:videoPath];
}
[[NSNotificationCenter defaultCenter]postNotificationName:@"didFinishSelectVideo" object:videoPath];
[self.navigationController dismissViewControllerAnimated:YES completion:nil];
}
}
友情链接
- GitHub 项目地址
- iOS 自定义相机 拍照+视频录制(一) 拍照篇