1、 媒体捕捉概念
-
理解捕捉媒体,需要先了解一些基本概念:
-
捕捉会话:
AVCaptureSession 是管理捕获活动并协调从输入设备到捕获输出的数据流的对象。 AVCaptureSession 用于连接输入和输出的资源,从物理设备如摄像头和麦克风等获取数据流,输出到一个或多个目的地。 AVCaptureSession 可以额外配置一个会话预设值(session preset),用于控制捕捉数据的格式和质量,预设值默认值为 AVCaptureSessionPresetHigh。
要执行实时捕获,需要实例化AVCaptureSession对象并添加适当的输入和输出。下面的代码片段演示了如何配置捕获设备来录制音频。
// Create the capture session.
let captureSession = AVCaptureSession()
// Find the default audio device.
guard let audioDevice = AVCaptureDevice.default(for: .audio) else { return }
do {
// Wrap the audio device in a capture device input.
let audioInput = try AVCaptureDeviceInput(device: audioDevice)
// If the input can be added, add it to the session.
if captureSession.canAddInput(audioInput) {
captureSession.addInput(audioInput)
}
} catch {
// Configuration failed. Handle error.
}
您可以调用startRunning()
来启动从输入到输出的数据流,并调用stopRunning()
来停止该流。
注意:
startRunning()
方法是一个阻塞调用,可能会花费一些时间,因此应该在串行队列上执行会话设置,以免阻塞主队列(这使UI保持响应)。参见AVCam:构建摄像机应用程序的实现示例。
-
捕捉设备:
AVCaptureDevice 是为捕获会话提供输入(如音频或视频)并为特定于硬件的捕获特性提供控制的设备。它为物理设备定义统一接口,以及大量控制方法,获取指定类型的默认设备方法如下:
self.activeVideoDevice = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];
一个 AVCaptureDevice 对象表示一个物理捕获设备和与该设备相关联的属性。您可以使用捕获设备来配置底层硬件的属性。捕获设备还向AVCaptureSession对象提供输入数据(如音频或视频)。
-
捕捉设备的输入:
不能直接将 AVCaptureDevice 加入到 AVCaptureSession 中,需要封装为 AVCaptureDeviceInput。
self.captureVideoInput = [AVCaptureDeviceInput deviceInputWithDevice:self.activeVideoDevice error:&videoError];
if (self.captureVideoInput) {
if ([self.captureSession canAddInput:self.captureVideoInput]){
[self.captureSession addInput:self.captureVideoInput];
}
} else if (videoError) {
}
-
捕捉输出 :
AVCaptureOutput 作为抽象基类提供了捕捉会话数据流的输出目的地,同时定义了此抽象类的高级扩展类。
AVCaptureStillImageOutput - 静态照片( 在ios10后被废弃,使用AVCapturePhotoOutput代替)
AVCaptureMovieFileOutput - 视频,
AVCaptureAudioDataOutput - 音频底层数字样本
AVCaptureVideoDataOutput - 视频底层数字样本
-
捕捉连接:
AVCaptureConnection :捕获会话中捕获输入和捕获输出对象的特定对之间的连接。AVCaptureConnection 用于确定哪些输入产生视频,哪些输入产生音频,能够禁用特定连接或访问单独的音频轨道。
捕获输入有一个或多个输入端口(avcaptureinpu . port的实例)。捕获输出可以接受来自一个或多个源的数据(例如,AVCaptureMovieFileOutput对象同时接受视频和音频数据)。 只有在canAddConnection(:)方法返回true时,才可以使用addConnection(:)方法将AVCaptureConnection实例添加到会话中。当使用addInput(:)或addOutput(:)方法时,会话自动在所有兼容的输入和输出之间形成连接。在添加没有连接的输入或输出时,只需手动添加连接。您还可以使用连接来启用或禁用来自给定输入或到给定输出的数据流。
-
捕捉预览 :
AVCaptureVideoPreviewLayer 是一个 CALayer 的子类,可以对捕捉视频数据进行实时预览。
2、视频捕捉实例
-
这个实例的项目代码点击这里下载:OC 视频捕获相机Demo
-
项目是OC编写的,主要功能实现在THCameraController中,如下图:
-
主要接口变量在头文件
THCameraController.h
里面:
#import <AVFoundation/AVFoundation.h>
extern NSString *const THThumbnailCreatedNotification;
@protocol THCameraControllerDelegate <NSObject>
// 1发生错误事件是,需要在对象委托上调用一些方法来处理
- (void)deviceConfigurationFailedWithError:(NSError *)error;
- (void)mediaCaptureFailedWithError:(NSError *)error;
- (void)assetLibraryWriteFailedWithError:(NSError *)error;
@end
@interface THCameraController : NSObject
@property (weak, nonatomic) id<THCameraControllerDelegate> delegate;
@property (nonatomic, strong, readonly) AVCaptureSession *captureSession;
// 2 用于设置、配置视频捕捉会话
- (BOOL)setupSession:(NSError **)error;
- (void)startSession;
- (void)stopSession;
// 3 切换不同的摄像头
- (BOOL)switchCameras;
- (BOOL)canSwitchCameras;
@property (nonatomic, readonly) NSUInteger cameraCount;
@property (nonatomic, readonly) BOOL cameraHasTorch; //手电筒
@property (nonatomic, readonly) BOOL cameraHasFlash; //闪光灯
@property (nonatomic, readonly) BOOL cameraSupportsTapToFocus; //聚焦
@property (nonatomic, readonly) BOOL cameraSupportsTapToExpose;//曝光
@property (nonatomic) AVCaptureTorchMode torchMode; //手电筒模式
@property (nonatomic) AVCaptureFlashMode flashMode; //闪光灯模式
// 4 聚焦、曝光、重设聚焦、曝光的方法
- (void)focusAtPoint:(CGPoint)point;
- (void)exposeAtPoint:(CGPoint)point;
- (void)resetFocusAndExposureModes;
// 5 实现捕捉静态图片 & 视频的功能
//捕捉静态图片
- (void)captureStillImage;
//视频录制
//开始录制
- (void)startRecording;
//停止录制
- (void)stopRecording;
//获取录制状态
- (BOOL)isRecording;
//录制时间
- (CMTime)recordedDuration;
@end
-
我们需要添加访问权限,如果没有获取到相机和麦克风权限,在设置 captureVideoInput 时就会出错。
/// 检测 AVAuthorization 权限
/// 传入待检查的 AVMediaType,AVMediaTypeVideo or AVMediaTypeAudio
/// 返回是否权限可用
- (BOOL)ifAVAuthorizationValid:(NSString *)targetAVMediaType grantedCallback:(void (^)())grantedCallback
{
NSString *mediaType = targetAVMediaType;
BOOL result = NO;
if ([AVCaptureDevice respondsToSelector:@selector(authorizationStatusForMediaType:)]) {
AVAuthorizationStatus authStatus = [AVCaptureDevice authorizationStatusForMediaType:mediaType];
switch (authStatus) {
case AVAuthorizationStatusNotDetermined: { // 尚未请求授权
[AVCaptureDevice requestAccessForMediaType:targetAVMediaType completionHandler:^(BOOL granted) {
dispatch_async(dispatch_get_main_queue(), ^{
if (granted) {
grantedCallback();
}
});
}];
break;
}
case AVAuthorizationStatusDenied: { // 明确拒绝
if ([mediaType isEqualToString:AVMediaTypeVideo]) {
[METSettingPermissionAlertView showAlertViewWithPermissionType:METSettingPermissionTypeCamera];// 申请相机权限
} else if ([mediaType isEqualToString:AVMediaTypeAudio]) {
[METSettingPermissionAlertView showAlertViewWithPermissionType:METSettingPermissionTypeMicrophone];// 申请麦克风权限
}
break;
}
case AVAuthorizationStatusRestricted: { // 限制权限更改
break;
}
case AVAuthorizationStatusAuthorized: { // 已授权
result = YES;
break;
}
default: // 兜底
break;
}
}
return result;
}
2.1 创建预览视图
-
可以直接向一个 view 的 layer 中加入一个 AVCaptureVideoPreviewLayer 对象:
self.previewLayer = [[AVCaptureVideoPreviewLayer alloc] init];
[self.previewLayer setVideoGravity:AVLayerVideoGravityResizeAspectFill];
[self.previewLayer setSession:self.cameraHelper.captureSession];
self.previewLayer.frame = CGRectMake(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT - 50);
[self.previewImageView.layer addSublayer:self.previewLayer];
-
也可以通过 view 的类方法直接换掉 view 的 CALayer 实例:
+ (Class)layerClass {
return [AVCaptureVideoPreviewLayer class];
}
- (AVCaptureSession*)session {
return [(AVCaptureVideoPreviewLayer*)self.layer session];
}
- (void)setSession:(AVCaptureSession *)session {
[(AVCaptureVideoPreviewLayer*)self.layer setSession:session];
}
-
AVCaptureVideoPreviewLayer 定义了两个方法用于在屏幕坐标系和设备坐标系之间转换,设备坐标系规定左上角为 (0,0),右下角为(1,1)。
(CGPoint)captureDevicePointOfInterestForPoint:(CGPoint)pointInLayer
从屏幕坐标系的点转换为设备坐标系
(CGPoint)pointForCaptureDevicePointOfInterest:(CGPoint)captureDevicePointOfInterest
从设备坐标系的点转换为屏幕坐标系
【学习地址】:FFmpeg/WebRTC/RTMP/NDK/Android音视频流媒体高级开发
【文章福利】:免费领取更多音视频学习资料包、大厂面试题、技术视频和学习路线图,资料包括(C/C++,Linux,FFmpeg webRTC rtmp hls rtsp ffplay srs 等等)有需要的可以点击1079654574加群领取哦~
2.2 设置捕捉会话
-
首先是初始化捕捉会话:
self.captureSession = [[AVCaptureSession alloc]init];
[self.captureSession setSessionPreset:(self.isVideoMode)?AVCaptureSessionPreset1280x720:AVCaptureSessionPresetPhoto];
-
根据拍摄视频还是拍摄照片选择不同的预设值,然后设置会话输入:
- (void)configSessionInput
{
// 摄像头输入
NSError *videoError = [[NSError alloc] init];
self.activeVideoDevice = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];
self.flashMode = self.activeVideoDevice.flashMode;
self.captureVideoInput = [AVCaptureDeviceInput deviceInputWithDevice:self.activeVideoDevice error:&videoError];
if (self.captureVideoInput) {
if ([self.captureSession canAddInput:self.captureVideoInput]){
[self.captureSession addInput:self.captureVideoInput];
}
} else if (videoError) {
}
if (self.isVideoMode) {
// 麦克风输入
NSError *audioError = [[NSError alloc] init];
AVCaptureDeviceInput *audioInput = [AVCaptureDeviceInput deviceInputWithDevice:[AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeAudio] error:&audioError];
if (audioInput) {
if ([self.captureSession canAddInput:audioInput]) {
[self.captureSession addInput:audioInput];
}
} else if (audioError) {
}
}
}
-
对摄像头和麦克风设备均封装为 AVCaptureDeviceInput 后加入到会话中。然后配置会话输出:
- (void)configSessionOutput
{
if (self.isVideoMode) {
// 视频输出
self.movieFileOutput = [[AVCaptureMovieFileOutput alloc] init];
if ([self.captureSession canAddOutput:self.movieFileOutput]) {
[self.captureSession addOutput:self.movieFileOutput];
}
} else {
// 图片输出
self.imageOutput = [[AVCaptureStillImageOutput alloc] init];
self.imageOutput.outputSettings = @{AVVideoCodecKey:AVVideoCodecJPEG};// 配置 outputSetting 属性,表示希望捕捉 JPEG 格式的图片
if ([self.captureSession canAddOutput:self.imageOutput]) {
[self.captureSession addOutput:self.imageOutput];
}
}
}
-
当然你也可以合成在一个方法里面直接设置捕获会话
- (BOOL)setupSession:(NSError **)error {
//创建捕捉会话。AVCaptureSession 是捕捉场景的中心枢纽
self.captureSession = [[AVCaptureSession alloc]init];
/*
AVCaptureSessionPresetHigh
AVCaptureSessionPresetMedium
AVCaptureSessionPresetLow
AVCaptureSessionPreset640x480
AVCaptureSessionPreset1280x720
AVCaptureSessionPresetPhoto
*/
//设置图像的分辨率
self.captureSession.sessionPreset = AVCaptureSessionPresetHigh;
//拿到默认视频捕捉设备 iOS系统返回后置摄像头
AVCaptureDevice *videoDevice = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];
//将捕捉设备封装成AVCaptureDeviceInput
//注意:为会话添加捕捉设备,必须将设备封装成AVCaptureDeviceInput对象
AVCaptureDeviceInput *videoInput = [AVCaptureDeviceInput deviceInputWithDevice:videoDevice error:error];
//判断videoInput是否有效
if (videoInput)
{
//canAddInput:测试是否能被添加到会话中
if ([self.captureSession canAddInput:videoInput])
{
//将videoInput 添加到 captureSession中
[self.captureSession addInput:videoInput];
self.activeVideoInput = videoInput;
}
}else
{
return NO;
}
//选择默认音频捕捉设备 即返回一个内置麦克风
AVCaptureDevice *audioDevice = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeAudio];
//为这个设备创建一个捕捉设备输入
AVCaptureDeviceInput *audioInput = [AVCaptureDeviceInput deviceInputWithDevice:audioDevice error:error];
//判断audioInput是否有效
if (audioInput) {
//canAddInput:测试是否能被添加到会话中
if ([self.captureSession canAddInput:audioInput])
{
//将audioInput 添加到 captureSession中
[self.captureSession addInput:audioInput];
}
}else
{
return NO;
}
//AVCaptureStillImageOutput 实例 从摄像头捕捉静态图片
self.imageOutput = [[AVCaptureStillImageOutput alloc]init];
//配置字典:希望捕捉到JPEG格式的图片
self.imageOutput.outputSettings = @{AVVideoCodecKey:AVVideoCodecJPEG};
//输出连接 判断是否可用,可用则添加到输出连接中去
if ([self.captureSession canAddOutput:self.imageOutput])
{
[self.captureSession addOutput:self.imageOutput];
}
//创建一个AVCaptureMovieFileOutput 实例,用于将Quick Time 电影录制到文件系统
self.movieOutput = [[AVCaptureMovieFileOutput alloc]init];
//输出连接 判断是否可用,可用则添加到输出连接中去
if ([self.captureSession canAddOutput:self.movieOutput])
{
[self.captureSession addOutput:self.movieOutput];
}
self.videoQueue = dispatch_queue_create("com.kongyulu.VideoQueue", NULL);
return YES;
}
2.3 启动, 停止会话
-
可以在一个 VC 的生命周期内启动和停止会话,由于这个操作是比较耗时的同步操作,因此建议在异步线程里执行此方法。如下:
- (void)startSession {
//检查是否处于运行状态
if (![self.captureSession isRunning])
{
//使用同步调用会损耗一定的时间,则用异步的方式处理
dispatch_async(self.videoQueue, ^{
[self.captureSession startRunning];
});
}
}
- (void)stopSession {
//检查是否处于运行状态
if ([self.captureSession isRunning])
{
//使用异步方式,停止运行
dispatch_async(self.videoQueue, ^{
[self.captureSession stopRunning];
});
}
}
2.4 切换摄像头
-
大多数 ios 设备都有前后两个摄像头,标识前后摄像头需要用到 AVCaptureDevicePositio