1. 简介

本文首要针对 WebRTC 在苹果设备(IOS 和 MACOS)上怎么完结相机收集进行深入剖析。特别是,咱们会具体查看所用的 API,以及这些 API 所扮演的角色。

1.1 阅读本文后,你将掌握:

  • 相机收集根底概念及 API 介绍
  • 怎么自定义视频巨细和图像格局?
  • 怎么设置帧率
  • 在前后摄像头之间怎么切换?
  • WebRTC 怎么与苹果的 API 进行交互完结相机收集
  • IOS 和 MACOS 代码的通用性

经过掌握上述知识点,您不仅能了解 WebRTC 在相机收集方面的功用和约束,还能更加灵敏地在自己的项目中实施相关功用。

2. 相机收集根底概念及 API 介绍

苹果经过 AVFoundation 结构供给了一系列与相机收集相关的 API,首要包括 AVCaptureSession, AVCaptureDevice, AVCaptureDeviceInput, 和 AVCaptureVideoDataOutput

它们的关系能够以这张架构图阐明:

WebRTC 源码分析 (五) 苹果设备相机采集

2.1 AVCaptureSession

装备收集行为并和谐从输入设备到收集输出的数据流的目标。

实例化 AVCaptureSession

#import <AVFoundation/AVFoundation.h>
// 初始化 AVCaptureSession
AVCaptureSession *captureSession = [[AVCaptureSession alloc] init];

以下是核心 API 介绍:

2.1.1 装备会话

API 阐明
beginConfiguration() 标记对正在运行的收集会话的装备进行更改的开端,以在单个原子更新中履行
commitConfiguration() 在单个原子更新中提交对正在运行的收集会话的装备的一项或多项更改。
// 开端装备会话
[captureSession beginConfiguration];
// ...(增加输入和输出等)
// 提交装备更改
[captureSession commitConfiguration];

2.1.2 设置会话 preset

API 阐明
canSetSessionPreset 确认是否能够运用指定的预设装备收集会话。
sessionPreset 指示输出的质量等级或比特率的预设值
if ([captureSession canSetSessionPreset:AVCaptureSessionPreset1280x720]) {
    [captureSession setSessionPreset:AVCaptureSessionPreset1280x720];
}

2.1.3 装备输入

API 阐明
inputs: [AVCaptureInput\] 向收集会话供给媒体数据的输入
canAddInput(AVCaptureInput) -> Bool 确认是否能够向会话增加输入。
addInput(AVCaptureInput) 向会话增加收集输入。
removeInput(AVCaptureInput) 从会话中删去输入。
// 获取默许摄像头
AVCaptureDevice *camera = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];
// 创立摄像头输入
NSError *error;
AVCaptureDeviceInput *cameraInput = [AVCaptureDeviceInput deviceInputWithDevice:camera error:&error];
// 增加摄像头输入到会话
if ([captureSession canAddInput:cameraInput]) {
    [captureSession addInput:cameraInput];
}

2.1.4 装备输出

API 阐明
outputs: [AVCaptureOutput\] 收集会话将其数据发送到的输出方针。
canAddOutput(AVCaptureOutput) -> Bool 确认是否能够将输出增加到会话。
addOutput(AVCaptureOutput) 将输出增加到收集会话。
removeOutput(AVCaptureOutput) 从收集会话中删去输出。
// 创立输出目标
AVCaptureVideoDataOutput *output = [[AVCaptureVideoDataOutput alloc] init];
// 装备输出目标的参数(省掉)
// 增加输出到会话
if ([captureSession canAddOutput:output]) {
    [captureSession addOutput:output];
}

2.1.5 衔接输入和输出

API 阐明
connections: [AVCaptureConnection\] 收集会话包括的输入和输出之间的衔接。
addConnection(AVCaptureConnection) 增加到收集会话的衔接。
addInputWithNoConnections(AVCaptureInput) 将收集输入增加到会话而不构成任何衔接。
addOutputWithNoConnections(AVCaptureOutput) 将收集输出增加到会话而不构成任何衔接。
removeConnection(AVCaptureConnection) 从会话中删去收集衔接。
AVCaptureAudioChannel 监督收集衔接中音频通道的均匀和峰值功率等级的目标。
// 获取会话的衔接
AVCaptureConnection *connection = [output connectionWithMediaType:AVMediaTypeVideo];
// 装备衔接的参数(例如视频方向,稳定性设置等)(省掉)

2.1.6 办理会话生命周期

API 阐明
startRunning() 开端经过收集管道的数据流。
stopRunning() 停止经过收集管道的数据流。
// 开端会话
[captureSession startRunning];
// 停止会话
[captureSession stopRunning];

2.2 AVCaptureDevice

用于拜访和操控物理设备,如摄像头。

相关 API:

API 阐明
AVCaptureDeviceDiscoverySession 查找与特定搜索条件匹配的收集设备的目标。
defaultDeviceWithDeviceType:mediaType:position: 回来指定设备类型、媒体类型和方位的默许设备。
defaultDeviceWithMediaType: 回来收集指定媒体类型的默许设备。
deviceWithUniqueID: 创立一个表明具有指定标识符的设备的目标。
AVCaptureDeviceWasConnectedNotification 当新的收集设备可用时体系发布的告诉。
AVCaptureDeviceWasDisconnectedNotification 当现有设备不可用时体系发布的告诉。

2.2.1 授权设备拜访

API 阐明
requestAccessForMediaType:completionHandler: 恳求用户答应应用程序收集特定类型的媒体。
authorizationStatusForMediaType: 回来授权状况,指示用户是否授予应用程序收集特定类型媒体的权限。
AVAuthorizationStatus 指示应用程序收集媒体授权状况的常量。

2.2.2 识别设备

API 阐明
uniqueID 仅有标识设备的标识符。
modelID 设备的型号标识符。
localizedName 显现在用户界面中的本地化设备称号。
manufacturer 设备制造商的人类可读字符串。
deviceType 设备的类型,例如内置麦克风或广角摄像头。
AVCaptureDeviceType 定义结构支撑的设备类型的结构。
position 收集设备硬件的物理方位。
AVCaptureDevicePosition 指示收集设备的物理方位的常量。

2.2.3 装备相机硬件

API 阐明
- lockForConfiguration: 恳求独占拜访权限以装备设备硬件特点。
- unlockForConfiguration 释放对设备硬件特点的独占操控。
AVCaptureDevice *camera = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];
NSError *error = nil;
// 确认装备
if ([camera lockForConfiguration:&error]) {
    // 履行硬件装备代码
    // 解锁装备
    [camera unlockForConfiguration];
} else {
    NSLog(@"Error locking for configuration: %@", error);
}
API 阐明
subjectAreaChangeMonitoringEnabled 一个布尔值,指示设备是否监督主题区域的更改。
AVCaptureDeviceSubjectAreaDidChangeNotification 当收集设备检测到视频主题区域产生重大变化时体系发布的告诉。
camera.subjectAreaChangeMonitoringEnabled = YES;
// 增加观察者
[[NSNotificationCenter defaultCenter] addObserver:self
                                         selector:@selector(subjectAreaDidChange:)
                                             	name:AVCaptureDeviceSubjectAreaDidChangeNotification
                                           object:camera];
API 阐明
Formats 装备收集格局和相机帧速率。
// 获取支撑的媒体格局
NSArray *formats = [camera formats];
AVCaptureDeviceFormat *selectedFormat = nil;
for (AVCaptureDeviceFormat *format in formats) {
    // 在这儿,你能够依据需求来挑选一个合适的媒体格局
    // 例如,依据分辨率或帧率来挑选
    CMFormatDescriptionRef formatDesc = format.formatDescription;
    NSLog(@"Format: %@", CFBridgingRelease(CMFormatDescriptionCopyDescription(formatDesc)));
    if (CMFormatDescriptionGetMediaSubType(formatDesc) == kCVPixelFormatType_420YpCbCr8BiPlanarFullRange) {
        selectedFormat = format;
        break;
    }
}
if (selectedFormat) {
    if ([camera lockForConfiguration:nil]) {
        camera.activeFormat = selectedFormat;
        [camera unlockForConfiguration];
    }
}
API 阐明
Focus 装备相机的主动对焦行为,或手动设置其镜头方位。
if ([camera isFocusModeSupported:AVCaptureFocusModeAutoFocus]) {
    camera.focusMode = AVCaptureFocusModeAutoFocus;
}
API 阐明
Exposure 装备相机的主动曝光行为,或手动操控其曝光设置。
if ([camera isExposureModeSupported:AVCaptureExposureModeAutoExpose]) {
    camera.exposureMode = AVCaptureExposureModeAutoExpose;
}
API 阐明
White Balance 装备摄像机的主动白平衡行为,或手动操控白平衡设置。
if ([camera isWhiteBalanceModeSupported:AVCaptureWhiteBalanceModeAutoWhiteBalance]) {
    camera.whiteBalanceMode = AVCaptureWhiteBalanceModeAutoWhiteBalance;
}
API 阐明
Lighting 装备设备闪光灯、手电筒和弱光设置。
if ([camera hasFlash]) {
    camera.flashMode = AVCaptureFlashModeAuto;
}
if ([camera hasTorch]) {
    camera.torchMode = AVCaptureTorchModeAuto;
}
API 阐明
Color 办理设备的 HDR 和颜色空间设置。
// 查看HDR是否可用,并设置
if ([camera isHDREnabled]) {
    if ([camera lockForConfiguration:nil]) {
        [camera setAutomaticallyAdjustsVideoHDREnabled:YES]; // 主动调整HDR
        [camera unlockForConfiguration];
    }
}
// 查看和设置颜色空间
if ([camera.activeFormat isVideoColorSpaceSupported:AVCaptureColorSpace_P3_D65]) {
    if ([camera lockForConfiguration:nil]) {
        camera.activeColorSpace = AVCaptureColorSpace_P3_D65; // 设置P3颜色空间
        [camera unlockForConfiguration];
    }
}
API 阐明
Zoom 装备设备缩放行为并查看硬件功用。
if ([camera respondsToSelector:@selector(setVideoZoomFactor:)]) {
    camera.videoZoomFactor = 2.0; // 示例缩放因子
}

2.3 AVCaptureDeviceInput

用于从收集设备(如摄像头或麦克风)供给媒体输入到收集会话(AVCaptureSession)的目标。这个类是 AVCaptureInput 的一个具体子类,首要用于将收集设备衔接到收集会话。

2.3.1 创立输入

API 阐明
deviceInputWithDevice:error: 回来指定收集设备的新输入。
initWithDevice:error: 为指定的收集设备创立输入。
// 获取默许的摄像头设备
AVCaptureDevice *camera = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];
NSError *error = nil;
// 运用deviceInputWithDevice:error:创立输入
AVCaptureDeviceInput *input = [AVCaptureDeviceInput deviceInputWithDevice:camera error:&error];
// 或许运用initWithDevice:error:办法
AVCaptureDeviceInput *input = [[AVCaptureDeviceInput alloc] initWithDevice:camera error:&error];

2.3.2 装备输入特点

API 阐明
unifiedAutoExposureDefaultsEnabled 一个布尔值,指示输入是否启用一致主动曝光默许值。
videoMinFrameDurationOverride 充任收集设备的活动视频最小帧持续时刻修改器的时刻值。
// 启用或禁用一致的主动曝光默许值
input.unifiedAutoExposureDefaultsEnabled = YES;
// 设置视频最小帧持续时刻修饰符
input.videoMinFrameDurationOverride = CMTimeMake(1, 30);  // 例如,设置为30fps

2.3.3 拜访设备

API 阐明
device 与此输入关联的收集设备。
portsWithMediaType:sourceDeviceType:sourceDevicePosition: 检索虚拟设备的组成设备端口以在多摄像机会话中运用。
// 拜访与此输入关联的收集设备
AVCaptureDevice *associatedDevice = input.device;
// 在多摄像机会话中,检索虚拟设备的组成设备端口
NSArray *ports = [input portsWithMediaType:AVMediaTypeVideo sourceDeviceType:AVCaptureDeviceTypeBuiltInWideAngleCamera sourceDevicePosition:AVCaptureDevicePositionBack];

2.4 AVCaptureVideoDataOutput

收集输出,用于记载视频并供给对视频帧的拜访以进行处理。承继至 AVCaptureOutput

2.4.1 装备视频捕捉

API 阐明
videoSettings 包括输出紧缩设置的字典。
alwaysDiscardsLateVideoFrames 指示是否在视频帧迟到时丢弃它们。
automaticallyConfiguresOutputBufferDimensions 一个布尔值,指示输出是否主动装备输出缓冲区的巨细。
deliversPreviewSizedOutputBuffers 一个布尔值,指示输出是否装备为供给预览巨细的缓冲区。
recommendedVideoSettingsForVideoCodecType:assetWriterOutputFileType: 回来适合收集要录制到具有指定编解码器和类型的文件的视频的视频设置字典。
recommendedVideoSettingsForAssetWriterWithOutputFileType: 指定与 AVAssetWriterInput 一同运用的引荐设置。
// 创立实例
AVCaptureVideoDataOutput *videoOutput = [[AVCaptureVideoDataOutput alloc] init];
// 装备输出设置
videoOutput.videoSettings = @{(id)kCVPixelBufferPixelFormatTypeKey : @(kCVPixelFormatType_32BGRA)};
// 是否丢弃迟到的帧
videoOutput.alwaysDiscardsLateVideoFrames = YES;
// 主动装备输出缓冲区巨细
videoOutput.automaticallyConfiguresOutputBufferDimensions = YES;
// 供给预览巨细的缓冲区
videoOutput.deliversPreviewSizedOutputBuffers = NO;

2.4.2 检索支撑的视频类型

API 阐明
availableVideoCVPixelFormatTypes 输出支撑的视频像素格局。
availableVideoCodecTypes 输出支撑的视频编解码器。
availableVideoCodecTypesForAssetWriterWithOutputFileType: 输出支撑将视频写入输出文件的视频编解码器。
AVVideoCodecType 一组常量,描述体系支撑视频收集的编解码器。
// 查看可用的像素格局和编解码器类型
NSArray *pixelFormats = videoOutput.availableVideoCVPixelFormatTypes;
NSArray *codecTypes = videoOutput.availableVideoCodecTypes;

2.4.3 接纳收集的视频数据

API 阐明
setSampleBufferDelegate:queue: 设置示例缓冲区托付和调用回调的行列。
sampleBufferDelegate 收集目标的托付。
sampleBufferCallbackQueue 体系调用托付回调的行列。
AVCaptureVideoDataOutputSampleBufferDelegate 从视频数据输出接纳样本缓冲区并监督其状况的办法。
// 设置输出的署理和行列
[videoOutput setSampleBufferDelegate:self queue:dispatch_get_main_queue()];
- (void)captureOutput:(AVCaptureOutput *)output didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection *)connection {
    // 处理 sampleBuffer
}

2.4.4 创立视频收集输出

API 阐明
init 创立新的视频文件输出。
new 创立新的视频文件输出。
// 运用init或new办法创立实例
AVCaptureVideoDataOutput *videoOutput = [[AVCaptureVideoDataOutput alloc] init];
// 或
AVCaptureVideoDataOutput *videoOutput = [AVCaptureVideoDataOutput new];

3. WebRTC 怎么与苹果的 API 进行交互完结相机收集

依据上面的收集根底概念和上一篇文章的视频流程剖析,咱们来看下在 webrtc 苹果端(IOS,MAC) 是怎么进行的相机收集:

一般的流程是这样的:

  1. 创立一个AVCaptureSession目标,

  2. 并且为该目标增加输入设备和视频数据输出目标。

  3. AVCaptureSession设置视频分辨率、帧率、图像格局等信息

  4. 开端收集

咱们来验证下这个流程,首要看一下在webrtc中的流程:

WebRTC 源码分析 (五) 苹果设备相机采集

其次当咱们点击 IOS 或许 MAC OS 上对应的 Call roomstart call (如下图)之后就会履行到 ARDAppClient createLocalVideoTracks 函数

WebRTC 源码分析 (五) 苹果设备相机采集

- (RTC_OBJC_TYPE(RTCVideoTrack) *)createLocalVideoTrack {
  ...
#if !TARGET_IPHONE_SIMULATOR
  if (self.isBroadcast) {
...
  } else {
    RTC_OBJC_TYPE(RTCCameraVideoCapturer) *capturer =
        [[RTC_OBJC_TYPE(RTCCameraVideoCapturer) alloc] initWithDelegate:source];
    [_delegate appClient:self didCreateLocalCapturer:capturer];
  }
#else
#if defined(__IPHONE_11_0) && (__IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_11_0)
...
#endif
#endif
  return [_factory videoTrackWithSource:source trackId:kARDVideoTrackId];
}

上面的代码首要是初始化 RTCCameraVideoCapturer 目标,其次是调用 ARDAppClientDelegate appClient 函数,咱们依次看下各自的完结:

3.1 开端收集:

- (instancetype)initWithDelegate:(__weak id<RTC_OBJC_TYPE(RTCVideoCapturerDelegate)>)delegate {
  // 第一步
  return [self initWithDelegate:delegate captureSession:[[AVCaptureSession alloc] init]];
}
- (instancetype)initWithDelegate:(__weak id<RTC_OBJC_TYPE(RTCVideoCapturerDelegate)>)delegate
                  captureSession:(AVCaptureSession *)captureSession {
  if (self = [super initWithDelegate:delegate]) {
    //第二步
    if (![self setupCaptureSession:captureSession]) {
      return nil;
    }
...
  return self;
}
- (BOOL)setupCaptureSession:(AVCaptureSession *)captureSession {
  NSAssert(_captureSession == nil, @"Setup capture session called twice.");
  _captureSession = captureSession;
#if defined(WEBRTC_IOS)
  //第三步
  _captureSession.sessionPreset = AVCaptureSessionPresetInputPriority;
  _captureSession.usesApplicationAudioSession = NO;
#endif
  //第四步
  [self setupVideoDataOutput];
  //第五步
  if (![_captureSession canAddOutput:_videoDataOutput]) {
    RTCLogError(@"Video data output unsupported.");
    return NO;
  }
  [_captureSession addOutput:_videoDataOutput];
  return YES;
}

能够看到上面的第一步,内部实例化了一个 AVCaptureSession 目标,也就验证了咱们开端所说的第一点,

咱们持续看第二 > 三步,第三步内部是上面根底介绍到的设置会话的初始值,这儿在 IOS 平台下设置了 AVCaptureSessionPresetInputPriority 是什么意思呢?它其实便是 AVCaptureSession 的一个预设值,它影响捕获会话的行为,特别是在一同运用多个捕获设备(例如摄像头和麦克风)时。这个预设值的作用是告诉捕获会话不要去强制操控音频和视频的输出设置,而是让已衔接的捕获设备自己来操控输出的质量等级。

这个做法的好处是,它答应每个捕获设备依据其硬件才能和装备来自主决定输出的质量和性能,而不受默许预设值(例如 30fps)的约束。这样,您能够更灵敏地操控不同设备的捕获质量,以满意特定的需求,而不用遭到全局设置的影响。

简而言之,AVCaptureSessionPresetInputPriority 让捕获设备自己决定输出的质量,而不受固定的帧率等约束。这对于需求定制化捕获行为的应用程序来说非常有用,因为它供给了更多的灵敏性和性能操控选项。

第四步是 setupVideoDataOutput 函数,咱们看下内部完结:

- (void)setupVideoDataOutput {
  //4.1 步
  AVCaptureVideoDataOutput *videoDataOutput = [[AVCaptureVideoDataOutput alloc] init];
  //4.2 步
  NSSet<NSNumber *> *supportedPixelFormats =
      [RTC_OBJC_TYPE(RTCCVPixelBuffer) supportedPixelFormats];
  NSMutableOrderedSet *availablePixelFormats =
      [NSMutableOrderedSet orderedSetWithArray:videoDataOutput.availableVideoCVPixelFormatTypes];
  [availablePixelFormats intersectSet:supportedPixelFormats];
  NSNumber *pixelFormat = availablePixelFormats.firstObject;
  NSAssert(pixelFormat, @"Output device has no supported formats.");
  _preferredOutputPixelFormat = [pixelFormat unsignedIntValue];
  _outputPixelFormat = _preferredOutputPixelFormat;
  //4.3 步
  videoDataOutput.videoSettings = @{(NSString *)kCVPixelBufferPixelFormatTypeKey : pixelFormat};
  videoDataOutput.alwaysDiscardsLateVideoFrames = NO;
  //4.4 步
  [videoDataOutput setSampleBufferDelegate:self queue:self.frameQueue];
  _videoDataOutput = videoDataOutput;
}

上面的 4.1步:

  • 便是咱们根底概念介绍到的 AVCaptureVideoDataOutput 它首要便是担任收集输出,用于记载视频并供给对视频帧的拜访以进行处理。承继至 AVCaptureOutput

上面的 4.2 步:

  • 经过supportedPixelFormats 函数获取 webrtc 内置的支撑的像素格局
+ (NSSet<NSNumber*>*)supportedPixelFormats {
  return [NSSet setWithObjects:@(kCVPixelFormatType_420YpCbCr8BiPlanarFullRange),
                               @(kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange),
                               @(kCVPixelFormatType_32BGRA),
                               @(kCVPixelFormatType_32ARGB),
                               nil];
}
  • videoDataOutput目标中获取了可用的视频像素格局。
  • 经过交集操作,将支撑的像素格局与可用的像素格局进行匹配,以便挑选一个合适的像素格局。
  • 假如没有可用的像素格局,将触发断语过错。

上面的 4.3 步:

  • 设置videoDataOutputvideoSettings特点,运用选定的像素格局来装备视频输出。
  • alwaysDiscardsLateVideoFrames特点设置为NO,这表明不会丢弃推迟的视频帧。

上面的 4.4 步:

  • 将当时目标设置为videoDataOutput的Sample Buffer署理,表明在捕获到新的视频帧数据时将调用当时目标的办法进行处理。
  • 运用self.frameQueue作为行列,这或许是一个用于处理视频帧数据的自定义行列。

总的来说,setupVideoDataOutput 这段代码首要用于装备捕获视频数据的输出格局和处理方式,保证选定的像素格局是可用的,并设置了恰当的署理和行列来处理捕获到的视频帧。

咱们持续看最上面的 第 5 步:

  • 其实便是将第 4.4 步装备好的 videoDataOutput 增加到 _captureSession 目标中去。这一步也就验证了最上面的第2步骤的视频数据输出目标的装备。

这上面便是 RTCCameraVideoCapturer initWithDelegate 的整个剖析,下面咱们接着剖析 appClient 函数。

- (void)appClient:(ARDAppClient*)client
    didCreateLocalCapturer:(RTC_OBJC_TYPE(RTCCameraVideoCapturer) *)localCapturer {
  _captureController =
      [[ARDCaptureController alloc] initWithCapturer:localCapturer
                                            settings:[[ARDSettingsModel alloc] init]];
      [_captureController startCapture];
}

首要是实例化 ARDCaptureController ,并初始化参数,最终经过内部的 startCapture 函数开启收集,该函数首要逻辑如下:

- (void)startCapture:(void (^)(NSError *))completion {
  AVCaptureDevicePosition position =
      _usingFrontCamera ? AVCaptureDevicePositionFront : AVCaptureDevicePositionBack;
  AVCaptureDevice *device = [self findDeviceForPosition:position];
  AVCaptureDeviceFormat *format = [self selectFormatForDevice:device];
  if (format == nil) {
    RTCLogError(@"No valid formats for device %@", device);
    NSAssert(NO, @"");
    return;
  }
  NSInteger fps = [self selectFpsForFormat:format];
  [_capturer startCaptureWithDevice:device format:format fps:fps completionHandler:completion];
}
  • 首要,它依据是否运用前置摄像头来确认捕获设备的方位(前置或后置)。
  • 接着,它经过调用findDeviceForPosition:办法查找对应方位的摄像头设备。
  • 然后,它调用selectFormatForDevice:办法来挑选捕获设备的格局。
  • 假如没有可用的格局,则记载过错信息,并触发断语,然后退出。
  • 最终,它调用selectFpsForFormat:办法来挑选帧率,并调用startCaptureWithDevice:format:fps:completionHandler:办法来发动捕获。

上面的函数有几处比较重要比如:

  1. 怎么挑选像素格局?
  2. 怎么挑选收集帧率?

这儿不独自打开阐明,后面第 4 小点会来一致回答文章开端的问题。

最终,咱们接着看 startCaptureWithDevice 函数

- (void)startCaptureWithDevice:(AVCaptureDevice *)device
                        format:(AVCaptureDeviceFormat *)format
                           fps:(NSInteger)fps
             completionHandler:(nullable void (^)(NSError *_Nullable error))completionHandler {
  _willBeRunning = YES;
  [RTC_OBJC_TYPE(RTCDispatcher)
      dispatchAsyncOnType:RTCDispatcherTypeCaptureSession
                    block:^{
                      RTCLogInfo("startCaptureWithDevice %@ @ %ld fps", format, (long)fps);
...
                      self.currentDevice = device;
                      NSError *error = nil;
                      //第一步
                      if (![self.currentDevice lockForConfiguration:&error]) {
...
                        return;
                      }
                      //第二步
                      [self reconfigureCaptureSessionInput];
                      //第三步
                      [self updateOrientation];
                      //第四步
                      [self updateDeviceCaptureFormat:format fps:fps];
                      //第五步
                      [self updateVideoDataOutputPixelFormat:format];
                      //第六步
                      [self.captureSession startRunning];
                      //第七步
                      [self.currentDevice unlockForConfiguration];
                      self.isRunning = YES;
                      if (completionHandler) {
                        completionHandler(nil);
                      }
                    }];
}

这个函数startCaptureWithDevice:format:fps:completionHandler:首要用于发动视频捕获,下面是对其不同步骤的剖析:

第一步: 确认捕获设备装备

  • 这是一项关键操作,它测验确认当时收集设备的装备,以便能够更改它。
  • 假如确认失败,它会生成一个过错,并直接回来,不履行后续的操作。

第二步: 重新装备捕获会话输入

- (void)reconfigureCaptureSessionInput {
  NSAssert([RTC_OBJC_TYPE(RTCDispatcher) isOnQueueForType:RTCDispatcherTypeCaptureSession],
           @"reconfigureCaptureSessionInput must be called on the capture queue.");
  NSError *error = nil;
  AVCaptureDeviceInput *input =
      [AVCaptureDeviceInput deviceInputWithDevice:_currentDevice error:&error];
  if (!input) {
    RTCLogError(@"Failed to create front camera input: %@", error.localizedDescription);
    return;
  }
  [_captureSession beginConfiguration];
  for (AVCaptureDeviceInput *oldInput in [_captureSession.inputs copy]) {
    [_captureSession removeInput:oldInput];
  }
  if ([_captureSession canAddInput:input]) {
    [_captureSession addInput:input];
  } else {
    RTCLogError(@"Cannot add camera as an input to the session.");
  }
  [_captureSession commitConfiguration];
}

这个函数 reconfigureCaptureSessionInput,首要用于重新装备捕获会话的输入,下面是这个函数的具体解释:

  1. 创立捕获设备输入:
    • 运用 _currentDevice 创立一个 AVCaptureDeviceInput 目标 input。这个 input 是一个表明摄像头设备输入的目标。
  2. 查看输入创立是否成功:
    • 假如创立 input 失败,即 _currentDevice 不可用或其他过错,会记载过错信息到日志,并直接回来函数,不再履行后续操作。
  3. 开端装备捕获会话:
    • 运用 _captureSessionbeginConfiguration 办法,开端装备捕获会话。在这之后,能够对会话进行装备操作,而这些装备操作将在调用 commitConfiguration 之后一同收效。
  4. 移除旧的输入:
    • 运用 for-in 循环遍历 _captureSession.inputs 中的所有输入(通常是之前的摄像头输入)。
    • 经过 _captureSessionremoveInput: 办法将每个旧输入从会话中移除。这是为了铲除之前或许存在的摄像头输入,以便替换为新的输入。
  5. 增加新的输入:
    • 查看 _captureSession 是否能够增加新的摄像头输入 input,假如能够,运用 _captureSessionaddInput: 办法将新的输入增加到会话中。这样就将当时设备的摄像头输入装备到了捕获会话中。
  6. 提交会话装备:
    • 运用 _captureSessioncommitConfiguration 办法,提交捕获会话的装备,使之收效。

该函数验证了开端说的步骤第2点的装备输入设备到 收集会话中。

第三步: 更新当时设备的旋转方向

- (void)updateOrientation {
  NSAssert([RTC_OBJC_TYPE(RTCDispatcher) isOnQueueForType:RTCDispatcherTypeCaptureSession],
           @"updateOrientation must be called on the capture queue.");
#if TARGET_OS_IPHONE
  _orientation = [UIDevice currentDevice].orientation;
#endif
}

依据设备的方向,会在收集输出的视频数据中更新方向。

第四步: 更新设备收集格局和帧率

- (void)updateDeviceCaptureFormat:(AVCaptureDeviceFormat *)format fps:(NSInteger)fps {
  NSAssert([RTC_OBJC_TYPE(RTCDispatcher) isOnQueueForType:RTCDispatcherTypeCaptureSession],
           @"updateDeviceCaptureFormat must be called on the capture queue.");
  @try {
    _currentDevice.activeFormat = format;
    _currentDevice.activeVideoMinFrameDuration = CMTimeMake(1, fps);
  } @catch (NSException *exception) {
    RTCLogError(@"Failed to set active format!\n User info:%@", exception.userInfo);
    return;
  }
}

第五步: 更新视频数据输出像素格局

- (void)updateVideoDataOutputPixelFormat:(AVCaptureDeviceFormat *)format {
  FourCharCode mediaSubType = CMFormatDescriptionGetMediaSubType(format.formatDescription);
  if (![[RTC_OBJC_TYPE(RTCCVPixelBuffer) supportedPixelFormats] containsObject:@(mediaSubType)]) {
    mediaSubType = _preferredOutputPixelFormat;
  }
  if (mediaSubType != _outputPixelFormat) {
    _outputPixelFormat = mediaSubType;
    _videoDataOutput.videoSettings =
        @{ (NSString *)kCVPixelBufferPixelFormatTypeKey : @(mediaSubType) };
  }
}

第六步: 发动捕获会话

  • 经过调用 [self.captureSession startRunning] 来发动捕获会话,这是实际开端捕获视频帧的地方。

第七步: 解锁捕获设备装备

  • 解锁之前确认的捕获设备的装备,以答应其他应用程序或操作运用设备。

这个函数首要担任视频捕获的发动进程,包括确认和装备摄像头设备、更新会话设置、发动会话以及在完结时告诉回调。这是一个多步骤的进程,经过异步分派在后台线程中履行,以保证不会堵塞主线程。

并且在这个函数中也验证了开端的 第三点和第4点流程,由此现在正是的开端收集了。

3.2 收集输出

在上面 3.1 末节中介绍了 setSampleBufferDelegate 函数,它的第一个参数便是收集回调

- (void)setSampleBufferDelegate:(nullable id<AVCaptureVideoDataOutputSampleBufferDelegate>)sampleBufferDelegate queue:(nullable dispatch_queue_t)sampleBufferCallbackQueue;
API_AVAILABLE(macos(10.7), ios(4.0), macCatalyst(14.0)) API_UNAVAILABLE(tvos) API_UNAVAILABLE(watchos)
@protocol AVCaptureVideoDataOutputSampleBufferDelegate <NSObject>
@optional
- (void)captureOutput:(AVCaptureOutput *)output didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection *)connection;
- (void)captureOutput:(AVCaptureOutput *)output didDropSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection *)connection API_AVAILABLE(ios(6.0), macCatalyst(14.0)) API_UNAVAILABLE(tvos);
@end

需求完结这两个函数,就能接纳收集视频的回调了,那么这两个函数有什么区别呢?

  • captureOutput:didOutputSampleBuffer:fromConnection: 这个办法首要用于处理捕获到的视频帧数据,它在 macOS 和 iOS 平台上可用,也支撑 Mac Catalyst。
  • captureOutput:didDropSampleBuffer:fromConnection: 这个办法在视频帧被丢弃(即丢失)时被调用,通常用于陈述帧的丢失情况。它在 iOS 和 Mac Catalyst 上可用,但不支撑 tvOS。

这儿咱们只重视视频帧输出,所以直接看第一个函数的完结:

#pragma mark AVCaptureVideoDataOutputSampleBufferDelegate
- (void)captureOutput:(AVCaptureOutput *)captureOutput
    didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer
           fromConnection:(AVCaptureConnection *)connection {
  //第一步
  NSParameterAssert(captureOutput == _videoDataOutput);
  if (CMSampleBufferGetNumSamples(sampleBuffer) != 1 || !CMSampleBufferIsValid(sampleBuffer) ||
      !CMSampleBufferDataIsReady(sampleBuffer)) {
    return;
  }
  CVPixelBufferRef pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer);
  if (pixelBuffer == nil) {
    return;
  }
#if TARGET_OS_IPHONE
  BOOL usingFrontCamera = NO;
  //第二步
  AVCaptureDevicePosition cameraPosition =
      [AVCaptureSession devicePositionForSampleBuffer:sampleBuffer];
  if (cameraPosition != AVCaptureDevicePositionUnspecified) {
    usingFrontCamera = AVCaptureDevicePositionFront == cameraPosition;
  } else {
    AVCaptureDeviceInput *deviceInput =
        (AVCaptureDeviceInput *)((AVCaptureInputPort *)connection.inputPorts.firstObject).input;
    usingFrontCamera = AVCaptureDevicePositionFront == deviceInput.device.position;
  }
  //第三步
  switch (_orientation) {
    case UIDeviceOrientationPortrait:
      _rotation = RTCVideoRotation_90;
      break;
    case UIDeviceOrientationPortraitUpsideDown:
      _rotation = RTCVideoRotation_270;
      break;
    case UIDeviceOrientationLandscapeLeft:
      _rotation = usingFrontCamera ? RTCVideoRotation_180 : RTCVideoRotation_0;
      break;
    case UIDeviceOrientationLandscapeRight:
      _rotation = usingFrontCamera ? RTCVideoRotation_0 : RTCVideoRotation_180;
      break;
    case UIDeviceOrientationFaceUp:
    case UIDeviceOrientationFaceDown:
    case UIDeviceOrientationUnknown:
      // Ignore.
      break;
  }
#else
  // No rotation on Mac.
  _rotation = RTCVideoRotation_0;
#endif
//第四步
  RTC_OBJC_TYPE(RTCCVPixelBuffer) *rtcPixelBuffer =
      [[RTC_OBJC_TYPE(RTCCVPixelBuffer) alloc] initWithPixelBuffer:pixelBuffer];
  int64_t timeStampNs = CMTimeGetSeconds(CMSampleBufferGetPresentationTimeStamp(sampleBuffer)) *
      kNanosecondsPerSecond;
  RTC_OBJC_TYPE(RTCVideoFrame) *videoFrame =
      [[RTC_OBJC_TYPE(RTCVideoFrame) alloc] initWithBuffer:rtcPixelBuffer
                                                  rotation:_rotation
                                               timeStampNs:timeStampNs];
  [self.delegate capturer:self didCaptureVideoFrame:videoFrame];
}

第一步:参数验证和有效性查看

  • 代码首要经过 NSParameterAssert 验证 captureOutput 是否等于 _videoDataOutput,保证回调是由正确的输出目标触发的。
  • 接着,它对 sampleBuffer 进行多项有效性查看:
    • 查看样本缓冲中的样本数是否为1,以保证只有一个样本。
    • 查看样本缓冲是否有效,以保证它不为空或损坏。
    • 查看样本缓冲数据是否准备就绪,以保证能够安全地拜访数据。

第二步:确认摄像头运用情况(IOS)

  • 代码依据摄像头的方位来确认是否运用前置摄像头。这儿有两种方式来确认:
    • 首要,它测验从 sampleBuffer 中获取摄像头方位信息,假如可用,则依据摄像头方位确认是否运用前置摄像头。
    • 假如无法从 sampleBuffer 中获取方位信息,则经过查看衔接的输入端口来判断是否运用前置摄像头。

第三步:确认视频旋转视点(IOS)

  • 依据设备的方向(_orientation)以及摄像头运用情况(前置或后置),确认视频的旋转视点(_rotation)。不同的设备方向和摄像头运用情况会导致不同的旋转视点,以保证视频帧的正确方向显现。

第四步:创立并传递视频帧

  • 代码创立了一个 RTCVideoFrame 目标,其间包括了视频数据(从 pixelBuffer 创立),旋转信息(从第三步确认的 _rotation),以及时刻戳信息(从样本缓冲的时刻戳获取)。
  • 最终,将收集的视频帧传递给署理目标(用于编码或许预览)。

总结:这段收集代码首要完结了视频捕获后的处理和传递。它首要验证和查看参数的有效性,然后依据摄像头运用情况和设备方向确认视频的旋转视点,最终将收集到的视频帧以署理的形式传递给编码或预览。经过这四个步骤完结了视频收集后的处理流程。

4. 回答

经过上面 3 大点的介绍,其实陆陆续续的已经介绍答案了。这儿咱们再进行总结一下吧

4.1 怎么自定义视频巨细和图像格局

经过如下代码获取当时设备的收集巨细和图像格局:

- (AVCaptureDeviceFormat *)selectFormatForDevice:(AVCaptureDevice *)device {
  NSArray<AVCaptureDeviceFormat *> *formats =
      [RTC_OBJC_TYPE(RTCCameraVideoCapturer) supportedFormatsForDevice:device];
    // 遍历并打印每个格局的信息
    for (AVCaptureDeviceFormat *format in formats) {
        CMFormatDescriptionRef formatDescription = format.formatDescription;
        // 获取格局的具体信息
        CMVideoDimensions dimensions = CMVideoFormatDescriptionGetDimensions(formatDescription);
        FourCharCode pixelFormat = CMFormatDescriptionGetMediaSubType(formatDescription);
        int frameRate = [format.videoSupportedFrameRateRanges.firstObject maxFrameRate]; // 获取最大帧率
        NSString *pixelFormatString = nil;
        // 打印格局信息
        switch (pixelFormat) {
            case kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange:
                pixelFormatString = @"NV12"; // YUV 4:2:0, planar, 8-bit, video-range
                break;
            case kCVPixelFormatType_420YpCbCr8BiPlanarFullRange:
                pixelFormatString = @"NV21"; // YUV 4:2:0, planar, 8-bit, full-range
                break;
            case kCVPixelFormatType_422YpCbCr8:
                pixelFormatString = @"YUYV"; // YUV 4:2:2, packed, 8-bit
                break;
            case kCVPixelFormatType_32BGRA:
                pixelFormatString = @"BGRA"; // RGB 32-bit
                break;
            case kCVPixelFormatType_32ARGB:
                pixelFormatString = @"ARGB"; // RGB 32-bit
                break;
            default:
                pixelFormatString = @"Unknown";
                break;
        }
        NSLog(@"supportedFormatsForDevice: Width=%d, Height=%d, PixelFormat=%@", dimensions.width, dimensions.height, pixelFormatString);
    }
  /**自定义输出的视频巨细,会挑选最为合适的*/
  int targetWidth = 1920;
  int targetHeight = 720;
  AVCaptureDeviceFormat *selectedFormat = nil;
  int currentDiff = INT_MAX;
  for (AVCaptureDeviceFormat *format in formats) {
    CMVideoDimensions dimension = CMVideoFormatDescriptionGetDimensions(format.formatDescription);
    FourCharCode pixelFormat = CMFormatDescriptionGetMediaSubType(format.formatDescription);
    int diff = abs(targetWidth - dimension.width) + abs(targetHeight - dimension.height);
    if (diff < currentDiff) {
      selectedFormat = format;
      currentDiff = diff;
    } else if (diff == currentDiff && pixelFormat == [_capturer preferredOutputPixelFormat]) {
      selectedFormat = format;
    }
  }
  return selectedFormat;
}

打印如下:

MAC M1:

supportedFormatsForDevice: Width=1920, Height=1080, PixelFormat=NV12
supportedFormatsForDevice: Width=1280, Height=720, PixelFormat=NV12
supportedFormatsForDevice: Width=1080, Height=1920, PixelFormat=NV12
supportedFormatsForDevice: Width=1760, Height=1328, PixelFormat=NV12
supportedFormatsForDevice: Width=640, Height=480, PixelFormat=NV12
supportedFormatsForDevice: Width=1328, Height=1760, PixelFormat=NV12
supportedFormatsForDevice: Width=1552, Height=1552, PixelFormat=NV12

IPhone:

supportedFormatsForDevice: Width=192, Height=144, PixelFormat=NV12
supportedFormatsForDevice: Width=192, Height=144, PixelFormat=NV21
supportedFormatsForDevice: Width=352, Height=288, PixelFormat=NV12
supportedFormatsForDevice: Width=352, Height=288, PixelFormat=NV21
supportedFormatsForDevice: Width=480, Height=360, PixelFormat=NV12
supportedFormatsForDevice: Width=480, Height=360, PixelFormat=NV21
supportedFormatsForDevice: Width=640, Height=480, PixelFormat=NV12
supportedFormatsForDevice: Width=640, Height=480, PixelFormat=NV21
supportedFormatsForDevice: Width=640, Height=480, PixelFormat=NV12
supportedFormatsForDevice: Width=640, Height=480, PixelFormat=NV21
supportedFormatsForDevice: Width=960, Height=540, PixelFormat=NV12
supportedFormatsForDevice: Width=960, Height=540, PixelFormat=NV21
supportedFormatsForDevice: Width=1024, Height=768, PixelFormat=NV12
supportedFormatsForDevice: Width=1024, Height=768, PixelFormat=NV21
supportedFormatsForDevice: Width=1280, Height=720, PixelFormat=NV12
supportedFormatsForDevice: Width=1280, Height=720, PixelFormat=NV21
supportedFormatsForDevice: Width=1280, Height=720, PixelFormat=NV12
supportedFormatsForDevice: Width=1280, Height=720, PixelFormat=NV21
supportedFormatsForDevice: Width=1440, Height=1080, PixelFormat=NV12
supportedFormatsForDevice: Width=1440, Height=1080, PixelFormat=NV21
supportedFormatsForDevice: Width=1920, Height=1080, PixelFormat=NV12
supportedFormatsForDevice: Width=1920, Height=1080, PixelFormat=NV21
supportedFormatsForDevice: Width=1920, Height=1440, PixelFormat=NV12
supportedFormatsForDevice: Width=1920, Height=1440, PixelFormat=NV21
supportedFormatsForDevice: Width=3088, Height=2320, PixelFormat=NV12
supportedFormatsForDevice: Width=3088, Height=2320, PixelFormat=NV21

由此看出,在苹果设备上最通用的其实是 nv12 像素格局,其间 IOS 经过遍历出来得到了 [NV12,NV21] , MAC 得到了 [NV12] 格局。

4.2 怎么设置帧率

- (NSInteger)selectFpsForFormat:(AVCaptureDeviceFormat *)format {
  Float64 maxSupportedFramerate = 0;
  for (AVFrameRateRange *fpsRange in format.videoSupportedFrameRateRanges) {
    maxSupportedFramerate = fmax(maxSupportedFramerate, fpsRange.maxFrameRate);
    NSLog(@"selectFpsForFormat %f", maxSupportedFramerate);
  }
  return fmin(maxSupportedFramerate, kFramerateLimit);
}
- (void)updateDeviceCaptureFormat:(AVCaptureDeviceFormat *)format fps:(NSInteger)fps {
  NSAssert([RTC_OBJC_TYPE(RTCDispatcher) isOnQueueForType:RTCDispatcherTypeCaptureSession],
           @"updateDeviceCaptureFormat must be called on the capture queue.");
  @try {
    _currentDevice.activeFormat = format;
    _currentDevice.activeVideoMinFrameDuration = CMTimeMake(1, fps);
  } @catch (NSException *exception) {
    RTCLogError(@"Failed to set active format!\n User info:%@", exception.userInfo);
    return;
  }
}

在 IPhone 和 MAC 上均输出最大的收集帧率为 30fps,所以咱们设置的时候需求依据 format.videoSupportedFrameRateRanges 范围去设置

4.3 前后摄像头之间怎么切换

  //拿到前后摄像头的 position
  AVCaptureDevicePosition position =
      _usingFrontCamera ? AVCaptureDevicePositionFront : AVCaptureDevicePositionBack;
  //依据 position 去找到对应的摄像头设备
  AVCaptureDevice *device = [self findDeviceForPosition:position];
- (AVCaptureDevice *)findDeviceForPosition:(AVCaptureDevicePosition)position {
  NSArray<AVCaptureDevice *> *captureDevices =
      [RTC_OBJC_TYPE(RTCCameraVideoCapturer) captureDevices];
  for (AVCaptureDevice *device in captureDevices) {
    if (device.position == position) {
      return device;
    }
  }
  return captureDevices[0];
}

4.4 IOS 和 MACOS 代码的通用性

  • 经过第三大点的介绍,除了一些特定的装备外(经过平台宏装备),根本上一套收集代码在 IOS 和 MACOS 是能够直接运用的.

5.总结

经过上面的 4 个大点,从收集根底到 webrtc 中的实际运用剖析,每一个点都根本剖析到了,现在你能够自己完结一套相机收集的功用了。

参考

  • Setting Up a Capture Session
  • objccn.io/issue-23-1/