前段时间项目开发过程中遇到一个需求,想做一个类似微信那样的小视频,然后在录制视频的自定义图层上播放。于是就研究了 AVFoundation 的一些东西。实际开发过程中也遇到了一些问题,所以在这里做下记录。另外参考了SBVideoCaptureDemo的源码。
使用AVCaptureSession、AVCaptureMovieFileOutput、AVCaptureDeviceInput、AVCaptureVideoPreviewLayer来录制视频,并通过AVAssetExportSeeion压缩视频并转换为 MP4 格式。
使用AVPlayerLayer、AVPlayer、AVPlayerItem、NSURL自定义播放视频
1、视频的录制
判断用户的设备对视频录制的支持情况
1、视频录制之前要先判断摄像头是否可用。
2、摄像头是否被授权。
自定义频录制
对所用的几个类做简单说明
AVCaptureSession:媒体(音、视频)捕获会话,负责把捕获的音视频数据输出到输出设备中。一个AVCaptureSession可以有多个输入输出流。
AVCaptureDevice:输入设备,包括麦克风、摄像头,通过该对象可以设置物理设备的一些属性(例如相机聚焦等)。
AVCaptureDeviceInput:设备输入数据管理对象,可以根据AVCaptureDevice创建对应的AVCaptureDeviceInput对象,该对象将会被添加到AVCaptureSession中管理。
AVCaptureVideoPreviewLayer:相机拍摄预览图层,是CALayer的子类,使用该对象可以看到视频录制效果,创建该对象需要指定对应的AVCaptureSession对象。
AVCaptureMovieFileOutput:视频输出流。把一个输入或者输出添加到AVCaptureSession之后AVCaptureSession就会在所有相符的输入、输出设备之间 建立连接(AVCaptionConnection)。
//此状态表示视频制作时的各个状态
typedefNS_ENUM(NSInteger, VideoState)
{
VideoStateFree = 0,
VideoStateWillStartRecord,
VideoStateDidStartRecord,
VideoStateWillEndRecord,
VideoStateDidEndRecord,
VideoStateWillStartMerge,
VideoStateDidStartMerge,
};
//与VideoState不同
//此状态表示用户操作时的状态,比如:已经开始录制、停止录制
typedefNS_ENUM(NSInteger, RecordOptState)
{
RecordOptStateFree = 0,
RecordOptStateBegin,
RecordOptStateEnd,
};
//录制时用户手指所处区域,可以用来判断是在录制区域还是在取消录制区域
typedefNS_ENUM(NSInteger, CurrentRecordRegion)
{
CurrentRecordRegionFree = 0,
CurrentRecordRegionRecord,
CurrentRecordRegionCancelRecord,
};
初始化相关设置
self.captureSession= [[AVCaptureSessionalloc]init];
AVCaptureDevice*frontCamera =nil;
AVCaptureDevice*backCamera =nil;
NSArray*cameras = [AVCaptureDevicedevicesWithMediaType:AVMediaTypeVideo];
for(AVCaptureDevice*cameraincameras) {
if(AVCaptureDevicePositionFront== camera.position) {//前置摄像头
frontCamera = camera;
}
elseif(AVCaptureDevicePositionBack== camera.position)
{
backCamera = camera;
}
//默认使用后摄像机
[backCamera lockForConfiguration:nil];//先锁定设备
if([backCamera isExposureModeSupported:AVCaptureExposureModeContinuousAutoExposure]) {
[backCamerasetExposureMode:AVCaptureExposureModeContinuousAutoExposure];//曝光量调节
}
if([backCameraisFocusModeSupported:AVCaptureFocusModeContinuousAutoFocus]) {//焦点CGPoint
[backCamerasetFocusMode:AVCaptureFocusModeContinuousAutoFocus];
}
[backCameraunlockForConfiguration];
[self.captureSessionbeginConfiguration];
//input device
self.videoDeviceInput= [AVCaptureDeviceInput deviceInputWithDevice:backCamera error:nil];
AVCaptureDeviceInput*audioDeviceInput = [AVCaptureDeviceInput deviceInputWithDevice:[AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeAudio]error:nil];
if([self.captureSessioncanAddInput:self.videoDeviceInput]) {
[self.captureSessionaddInput:self.videoDeviceInput];
}
if([self.captureSessioncanAddInput:audioDeviceInput]) {
[self.captureSessionaddInput:audioDeviceInput];
}
//output device
self.movieFileOutput= [[AVCaptureMovieFileOutputalloc]init];
if([self.captureSessioncanAddOutput:self.movieFileOutput]) {
[self.captureSessionaddOutput:self.movieFileOutput];
}
//preset
if([self.captureSessioncanSetSessionPreset:AVCaptureSessionPreset640x480]) {
self.captureSession.sessionPreset=AVCaptureSessionPreset640x480;//AVCaptureSessionPresetLow
}
//preview layer
self.preViewLayer= [AVCaptureVideoPreviewLayerlayerWithSession:self.captureSession];
self.preViewLayer.videoGravity=AVLayerVideoGravityResizeAspectFill;
[self.captureSession commitConfiguration];
[self.captureSession startRunning];//会话 开始运行
注意:改变设备属性前一定要首先调用lockForConfiguration方法加锁,调用完之后使用unlockForConfiguration方法解锁。对相机设置时,要判断当前设备是否支持改设置。比如:isExposureModeSupported、isFocusModeSupported等。
//开始录制
- (void)startRecordingToOutputFileURL
{
_videoState=VideoStateWillStartRecord;
_recordOptState=RecordOptStateBegin;
//根据设备输出获得连接
AVCaptureConnection*captureConnection = [self.movieFileOutputconnectionWithMediaType:AVMediaTypeVideo];
//根据连接取得设备输出的数据
if(![self.movieFileOutputisRecording]) {
//预览图层和视频方向保持一致
captureConnection.videoOrientation= [self.preViewLayerconnection].videoOrientation;
[self.movieFileOutput startRecordingToOutputFileURL:[NSURL fileURLWithPath:[self getVideoSaveFilePathString]] recordingDelegate:self];//开始录制
}
else
}
{
[selfstopCurrentVideoRecording];
}
//停止录制
- (void)stopCurrentVideoRecording
{
[self stopCountDurTimer];//停止计时器
_videoState=VideoStateWillEndRecord;
[self.movieFileOutput stopRecording];//停止录制
}
#pragma mark - AVCaptureFileOutputRecordingDelegate
- (void)captureOutput:(AVCaptureFileOutput *)captureOutput didStartRecordingToOutputFileAtURL:(NSURL *)fileURL fromConnections:(NSArray *)connections
{
_videoState = VideoStateDidStartRecord;
self.videoSaveFilePath = [fileURL absoluteString];
self.currentFileURL = fileURL;
self.currentVideoDur = 0.0f;
self.totalVideoDur = 0.0f;
[self startCountDurTimer];//启动录制计时器
//这里抛出开始录制 代理
if (RecordOptStateEnd == _recordOptState) {//时间太短,还没开始录制,就已经松开了录制按钮,要停止正在录制的视频
[self stopCurrentVideoRecording];
}
}
- (void)captureOutput:(AVCaptureFileOutput *)captureOutput didFinishRecordingToOutputFileAtURL:(NSURL *)outputFileURL fromConnections:(NSArray *)connections error:(NSError *)error
{
_videoState = VideoStateDidEndRecord;
self.totalVideoDur += _currentVideoDur;
//这里抛出录制完成 代理
if (CurrentRecordRegionRecord == [self getCurrentRecordRegion]) {
if (self.totalVideoDur < MIN_VIDEO_DUR) {//录制时间太短
[self removeMovFile];//移除mov格式的视频文件
_videoState = VideoStateFree;
}
}
else
{
[self removeMovFile];//移除mov格式的视频文件
_videoState = VideoStateFree;
}
}
//将mov格式转化成MP4
- (void)mergeAndExportVideosAtFileURLs:(NSArray*)fileURLArray
{
_videoState = VideoStateWillStartMerge;
NSError *error = nil;
//渲染尺寸
CGSize renderSize = CGSizeMake(0, 0);
NSMutableArray *layerInstructionArray = [NSMutableArray array];
//用来合成视频
AVMutableComposition *mixComposition = [[AVMutableComposition alloc] init];
CMTime totalDuration = kCMTimeZero;
//先取assetTrack 也为了取renderSize
NSMutableArray *assetTrackArray = [NSMutableArray array];
NSMutableArray *assetArray = [NSMutableArray array];
for (NSURL *fileURL in fileURLArray) {
//AVAsset:素材库里的素材
AVAsset *asset = [AVAsset assetWithURL:fileURL];
if (!asset) {
continue;
}
[assetArray addObject:asset];
//素材的轨道
AVAssetTrack *assetTrack = [[asset tracksWithMediaType:AVMediaTypeVideo] objectAtIndex:0];//返回一个数组AVAssetTracks资产
[assetTrackArray addObject:assetTrack];
renderSize.width = MAX(renderSize.width, assetTrack.naturalSize.height);
renderSize.height = MAX(renderSize.height, assetTrack.naturalSize.width);
}
CGFloat renderW = 320;//MIN(renderSize.width, renderSize.height);
for (NSInteger i = 0; i < [assetArray count] && i < assetTrackArray.count; i++) {
AVAsset *asset = [assetArray objectAtIndex:i];
AVAssetTrack *assetTrack = [assetTrackArray objectAtIndex:i];
//文件中的音频轨道,里面可以插入各种对应的素材
AVMutableCompositionTrack *audioTrack = [mixComposition addMutableTrackWithMediaType:AVMediaTypeAudio preferredTrackID:kCMPersistentTrackID_Invalid];
NSArray*dataSourceArray= [asset tracksWithMediaType:AVMediaTypeAudio];//获取声道,即麦克风相关信息
[audioTrack insertTimeRange:CMTimeRangeMake(kCMTimeZero, asset.duration) ofTrack:((dataSourceArray.count > 0)?[dataSourceArray objectAtIndex:0]:nil) atTime:totalDuration error:nil];
//工程文件中的轨道,有音频轨,里面可以插入各种对应的素材
AVMutableCompositionTrack *videoTrack = [mixComposition addMutableTrackWithMediaType:AVMediaTypeVideo preferredTrackID:kCMPersistentTrackID_Invalid];
[videoTrack insertTimeRange:CMTimeRangeMake(kCMTimeZero, asset.duration) ofTrack:assetTrack atTime:totalDuration error:&error];
//视频轨道中的一个视频,可以缩放、旋转等
AVMutableVideoCompositionLayerInstruction *layerInstrucition = [AVMutableVideoCompositionLayerInstruction videoCompositionLayerInstructionWithAssetTrack:videoTrack];
totalDuration = CMTimeAdd(totalDuration, asset.duration);
CGFloat rate = renderW / MIN(assetTrack.naturalSize.width, assetTrack.naturalSize.height);
CGAffineTransform layerTransform = CGAffineTransformMake(assetTrack.preferredTransform.a, assetTrack.preferredTransform.b, assetTrack.preferredTransform.c, assetTrack.preferredTransform.d, assetTrack.preferredTransform.tx * rate, assetTrack.preferredTransform.ty * rate);
layerTransform = CGAffineTransformConcat(layerTransform, CGAffineTransformMake(1, 0, 0, 1, 0, -(assetTrack.naturalSize.width - assetTrack.naturalSize.height) / 2.0));//向上移动取中部影相
layerTransform = CGAffineTransformScale(layerTransform, rate, rate);//放缩,解决前后摄像结果大小不对称
[layerInstrucition setTransform:layerTransform atTime:kCMTimeZero];
[layerInstrucition setOpacity:0.0 atTime:totalDuration];
//data
[layerInstructionArray addObject:layerInstrucition];
}
//get save path
NSURL *mergeFileURL = [NSURL fileURLWithPath:[self getVideoMergeFilePathString]];
//export
AVMutableVideoCompositionInstruction *mainInstruction = [AVMutableVideoCompositionInstruction videoCompositionInstruction];
mainInstruction.timeRange = CMTimeRangeMake(kCMTimeZero, totalDuration);
mainInstruction.layerInstructions = layerInstructionArray;
AVMutableVideoComposition *mainCompositionInst = [AVMutableVideoComposition videoComposition];
mainCompositionInst.instructions = @[mainInstruction];
mainCompositionInst.frameDuration = CMTimeMake(1, 100);
// mainCompositionInst.renderSize = CGSizeMake(renderW, renderW * (sH/sW));
mainCompositionInst.renderSize = CGSizeMake(renderW, renderW * 0.75);//4:3比列
//资源导出
AVAssetExportSession *exporter = [[AVAssetExportSession alloc] initWithAsset:mixComposition presetName:AVAssetExportPresetMediumQuality];
exporter.videoComposition = mainCompositionInst;
exporter.outputURL = mergeFileURL;
exporter.outputFileType = AVFileTypeMPEG4;//视频格式MP4
exporter.shouldOptimizeForNetworkUse = YES;
[exporter exportAsynchronouslyWithCompletionHandler:^{
dispatch_async(dispatch_get_main_queue(), ^{
_videoState = VideoStateDidStartMerge;
//抛出转换成功 代理
[self removeMovFile];//移除MOV格式视频
});
}];
}
//计算视频大小
- (NSInteger) getFileSize:(NSString*) path
{
path = [pathstringByReplacingOccurrencesOfString:@"file://"withString:@""];
NSFileManager* filemanager = [NSFileManagerdefaultManager];
if([filemanagerfileExistsAtPath:path]){
NSDictionary* attributes = [filemanagerattributesOfItemAtPath:patherror:nil];
NSNumber*theFileSize;
if( (theFileSize = [attributesobjectForKey:NSFileSize]) )
return[theFileSizeintValue]/1024;
else
return-1;
}
else
{
return-1;
}
}
//拉近、拉远镜头
- (void)changeDeviceVideoZoomFactor
{
AVCaptureDevice*backCamera = [selfgetCameraDevice:NO];
CGFloatcurrent = 1.0;
if(1.0 == backCamera.videoZoomFactor) {
current = 2.0f;
if(current > backCamera.activeFormat.videoMaxZoomFactor) {
current = backCamera.activeFormat.videoMaxZoomFactor;
}
}
NSError*error =nil;
if([backCameralockForConfiguration:&error]) {
[backCamerarampToVideoZoomFactor:currentwithRate:10];
[backCameraunlockForConfiguration];
}
else
{
NSLog(@"锁定设备过程error,错误信息:%@",error.localizedDescription);
}
}
- (AVCaptureDevice*)getCameraDevice:(BOOL)isFront
{
NSArray*cameras = [AVCaptureDevicedevicesWithMediaType:AVMediaTypeVideo];
AVCaptureDevice*frontCamera;
AVCaptureDevice*backCamera;
for(AVCaptureDevice*cameraincameras) {
if(AVCaptureDevicePositionFront== camera.position) {
frontCamera = camera;
}
elseif(AVCaptureDevicePositionBack== camera.position)
{
backCamera = camera;
}
}
if(isFront) {
returnfrontCamera;
}
returnbackCamera;
}
2、自定义播放视频
//注意:播放视频的URL是fileURLWithPath。格式是:“file://var”
- (instancetype)initVideoFileURL:(NSURL*)videoFileURL withFrame:(CGRect)frame withView:(UIView*)view
{
self= [superinit];
if(self) {
self.videoFileURL= videoFileURL;
[selfregisterNotficationMessage];
[selfinitPlayLayer:framewithView:view];
}
returnself;
}
- (void)initPlayLayer:(CGRect)rect withView:(UIView*)view
{
if(!_videoFileURL) {
return;
}
AVAsset*asset = [AVURLAssetURLAssetWithURL:_videoFileURLoptions:nil];
self.playerItem= [AVPlayerItemplayerItemWithAsset:asset];
//self.player = [AVPlayer playerWithPlayerItem:self.playerItem];
self.player= [[AVPlayeralloc]init];
self.playerLayer= [AVPlayerLayerplayerLayerWithPlayer:self.player];
[self.playersetVolume:0.0f];//静音
[self.playerseekToTime:kCMTimeZero];
[self.playersetActionAtItemEnd:AVPlayerActionAtItemEndNone];
[self.playerreplaceCurrentItemWithPlayerItem:self.playerItem];
self.playerLayer.frame= rect;
self.playerLayer.videoGravity=AVLayerVideoGravityResizeAspectFill;
[view.layeraddSublayer:self.playerLayer];
}
- (void)playSight
{
[self.playerItemseekToTime:kCMTimeZero];
[self.playerplay];
}
- (void)pauseSight
{
[self.playerItemseekToTime:kCMTimeZero];
[self.playerpause];
}
- (void)releaseVideoPlayer
{
[selfremoveNotificationMessage];
if(self.player) {
[self.playerpause];
[self.playerreplaceCurrentItemWithPlayerItem:nil];
}
if(self.playerLayer) {
[self.playerLayerremoveFromSuperlayer];
}
self.player=nil;
self.playerLayer=nil;
self.playerItem=nil;
self.videoFileURL=nil;
}
#pragma mark - notification message
- (void)registerNotficationMessage
{
[[NSNotificationCenterdefaultCenter]addObserver:selfselector:@selector(avPlayerItemDidPlayToEnd:)name:AVPlayerItemDidPlayToEndTimeNotificationobject:nil];
}
- (void)removeNotificationMessage
{
[[NSNotificationCenterdefaultCenter]removeObserver:selfname:AVPlayerItemDidPlayToEndTimeNotificationobject:nil];
}
- (void)avPlayerItemDidPlayToEnd:(NSNotification*)notification
{
if(notification.object!=self.playerItem) {
return;
}
[self.playerItemseekToTime:kCMTimeZero];
[self.playerplay];
}
有关AVFoundation的知识点,还有很多。以后如果有其它的需求再做研究。