涉及硬件的音视频本事,比如采集、渲染、硬件编码、硬件解码,通常是与客户端操作体系强相关的,就算是跨平台的多媒体框架也必须利用平台原生语言的模块来支持这些功能
本系列文章将详细报告移动端音视频的采集、渲染、硬件编码、硬件解码这些涉及硬件的本事该怎样实现
本文为该系列文章的第 1 篇,将详细报告在 iOS 平台下怎样实现摄像头的视频采集
前言
视频采集,从编程的角度来看,也就是拿到摄像头采集到的图像数据,至于拿到数据之后的用途,可以五花八门,想干嘛就干嘛,比如:存储为照片、写入本地文件、编码后进行传输、本地预览
CMSampleBuffer
在开始之前,必须先了解 CMSampleBuffer 的概念,它可以简单理解为媒体数据之外加了一层封装,在视频相关场景下,其可以包含未编码的视频数据(CVPixelBuffer),也可以包含编码过的视频数据(CMBlockBuffer)
CMSampleBuffer 构成部门
- CMTime:图像的时间
- CMVideoFormatDescription:图像格式的形貌
- CMBlockBuffer or CVPixelBuffer:编码后的图像数据 or 未编码的图像数据
整体流程
申请摄像头权限
真正开始视频采集之前,需要在应用层向用户申请摄像头权限
在 App 的 info.plist 中添加键值对,key 为 “Privacy - Camera Usage Description”,value 为申请摄像头权限的原因
在启动视频采集之前,检查摄像头权限
- 假如处于“权限未定”状态,需要调用体系 API 进行权限申请,有结果之后再根据权限确定后续流程
- 假如处于“已授权”状态,走正常流程
- 假如处于“未授权”状态,走异常流程,UI 上可能要引导用户自行到手机的设置中打开本应用的摄像头权限
- AVAuthorizationStatus status = [AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeVideo];
- if (status == AVAuthorizationStatusNotDetermined) {
- // 权限未定,进行申请
- [AVCaptureDevice requestAccessForMediaType:AVMediaTypeVideo completionHandler:completionHandler];
- } else if (status == AVAuthorizationStatusAuthorized) {
- // 已授权,走正常流程
- } else {
- // 未授权,走异常流程
- }
复制代码
初始化 + 参数设置
AVCaptureSession
针对视频采集,Apple 只给了一套 API,就是 AVCaptureSession,十分简单明了
AVCaptureSession 的运行需要有 input 和 output
input 通常与摄像头设备关联,也就是 AVCaptureDeviceInput
output 可以有多种范例,本文将着重介绍 AVCaptureVideoDataOutput,就是能直接拿到原始视频数据的 output 范例,其他范例比如 AVCaptureStillImageOutput、AVCaptureMovieFileOutput 都是在原始数据的底子上满足了个性化的需求,比方:照相、视频存本地
AVCaptureSession 配置完成后,调用 startRunning 接口即可开始视频采集
因此要实现视频采集,AVCaptureSession 简单理解是这个样子
采集启动之后,图像数据的流向可以简单理解为这个样子
AVCaptureSession 常用的接口
- startRunning:开始采集
- stopRunning:停止采集
- beginConfiguration:开始配置
- commitConfiguration:竣事配置
AVCaptureDeviceInput
要创建 input,首先要拿到 AVCaptureDevice,可以理解为摄像头设备在代码中的抽象体现
device 对象的获取:在 iOS 10 及以上,建议利用 AVCaptureDeviceDiscoverySession;iOS 10 以下,利用 devicesWithMediaType 方法即可
- NSArray* device_list = nil;
- if (@available(iOS 10.0, *)) {
- NSArray* device_type_list = @[AVCaptureDeviceTypeBuiltInWideAngleCamera];
- AVCaptureDeviceDiscoverySession* device_discovery_session = [AVCaptureDeviceDiscoverySession discoverySessionWithDeviceTypes:device_type_list mediaType:AVMediaTypeVideo position:AVCaptureDevicePositionUnspecified];
- device_list = deviceDiscoverySession.devices;
- } else {
- device_list = [AVCaptureDevice devicesWithMediaType:AVMediaTypeVideo];
- }
复制代码
接着选取需要的 device 对象,创建 AVCaptureDeviceInput,将 input 对象关联到 AVCaptureSession
- NSError* error = nil;
- AVCaptureDeviceInput* device_input = [[AVCaptureDeviceInput alloc] initWithDevice:self.device error:&error];
- if (error) {
- // error logic
- }
复制代码
将 input 对象关联到 AVCaptureSession
- self.device_input = device_input;
- if ([self.capture_session canAddInput:self.device_input]) {
- [self.capture_session addInput:self.device_input];
- } else {
- // error logic
- }
复制代码
AVCaptureVideoDataOutput
创建 AVCaptureVideoDataOutput
- self.data_output = [[AVCaptureVideoDataOutput alloc] init];
复制代码
配置原始视频数据的格式,利用 NV12,也是 Apple 官方保举的格式,该格式在 iOS 中效率最高
- NSNumber* format_value = [NSNumber numberWithInt:kCVPixelFormatType_420YpCbCr8BiPlanarFullRange];
- NSDictionary *settings = [NSDictionary dictionaryWithObjectsAndKeys:format_value, kCVPixelBufferPixelFormatTypeKey, nil];
- [self.data_output setVideoSettings:settings];
复制代码
配置视频数据的署理对象和输出线程,需要指定一个串行队列,也就是另开一个线程来做视频数据的吸收。署理对象实现的方法参考 AVCaptureVideoDataOutputSampleBufferDelegate
- dispatch_queue_t video_output_queue = dispatch_queue_create("com.xxx.video.output.queue", DISPATCH_QUEUE_SERIAL);
- [self.data_output setSampleBufferDelegate:self queue:video_output_queue];
复制代码
将 output 对象关联到 AVCaptureSession
- if ([self.capture_session canAddOutput:self.data_output]) {
- [self.capture_session addOutput:self.data_output];
- } else {
- // error logic
- }
复制代码
在其他的教程中,可能会出现利用 AVCaptureConnection 来继承配置输出参数,最常见的有视频的方向和镜像,但出于性能思量,不建议利用 AVCaptureConnection 来做,由于视频采集在常见的音视频业务场景里只是一个巨大 pipeline 下的最初始环节,应该尽量制止一些额外的耗时操作
当然,针对一些简单的场景,可以通过 AVCaptureConnection 直接指定视频方向和是否镜像
- self.data_output_connection = [self.data_output connectionWithMediaType:AVMediaTypeVideo];
- if (self.dataOutputConnection.isVideoOrientationSupported) {
- self.data_output_connection.videoOrientation = AVCaptureVideoOrientationPortrait;
- }
- if (self.dataOutputConnection.isVideoMirroringSupported) {
- // iOS 和 macOS 平台,isVideoMirroringSupported 都会返回 YES
- // 但是设置 videoMirrored 之后,只有 iOS 上使用前置摄像头时才会生效
- self.data_output_connection.videoMirrored = YES;
- }
复制代码
以视频的方向为例,设备竖屏放置时,摄像头采集出来的画面其实是横屏的,需要顺时针旋转 90 度才是预期内竖屏的画面
通过 AVCaptureConnection 可以让体系帮忙把旋转 90 度的操作在采集阶段做掉,但会引入性能消耗,因此保举放在后续环节。具体细节可参考 Apple 官方文档 https://developer.apple.com/documentation/avfoundation/avcaptureconnection/1389415-videoorientation?language=objc
配置分辨率和帧率
最简单的方法,通过 AVCaptureSession setSessionPreset 方法设置体系预设的分辨率和帧率,体系提供的 preset 在字面意思中只会体现分辨率信息,不体现帧率,帧率通常是 30
常见的 preset
- AVCaptureSessionPreset640x480
- AVCaptureSessionPreset1280x720
- AVCaptureSessionPreset1920x1080
进阶一点的方法,当我们想实现分辨率和帧率的自由组合,可以通过 device 对象的 formats 属性去探求,找到合适的之后,再设置给 device
注意:根据 AVCaptureVideoDataOutput 章节提到的采集画面默认为横屏的特点,分辨率宽高应该按照横屏进行设置
- uint32_t target_fps = 60;
- uint32_t target_width = 1920;
- uint32_t target_height = 1080;
- for (AVCaptureDeviceFormat *format in self.device.formats) {
- NSUInteger max_frame_rate = format.videoSupportedFrameRateRanges.firstObject.maxFrameRate;
- if (max_frame_rate < target_fps) {
- continue;
- }
-
- NSArray<AVFrameRateRange *> *range_list = format.videoSupportedFrameRateRanges;
- for (AVFrameRateRange *range in range_list) {
- if (target_fps == (NSUInteger)range.maxFrameRate) {
- // 匹配到了想要的帧率
- CMFormatDescriptionRef description = format.formatDescription;
- CMVideoDimensions dimensions = CMVideoFormatDescriptionGetDimensions(description);
- if (dimensions.width == target_width &&
- dimensions.height == target_height) {
- // 匹配到了想要的分辨率
- [self.device setActiveFormat:format];
- [self.device setActiveVideoMinFrameDuration:range.minFrameDuration];
- [self.device setActiveVideoMaxFrameDuration:range.minFrameDuration];
- return;
- }
- }
- }
- }
- // logic:没匹配到
复制代码
注意事项
配置参数的流程需要额外注意,AVCaptureDevice 和 AVCaptureSession 不是随时随地都能进行配置的
- AVCaptureDevice 需要先锁定、再修改参数、末了解锁
- AVCaptureSession 需要先开始配置、再修改参数、末了竣事配置
因此配置阶段的流程大抵如下
- // 锁定 device,开始配置
- [self.device lockForConfiguration:NULL];
- // 开始配置 AVCaptureSession
- [self.captureSession beginConfiguration];
- // 配置 input
- // 配置 output
- // 结束配置 AVCaptureSession
- [self.captureSession commitConfiguration];
- // 针对 device 设置分辨率和帧率
- // 解锁 device,结束配置
- [self.device unlockForConfiguration];
复制代码
处置惩罚数据回调
采集开始后数据会通过 AVCaptureVideoDataOutputSampleBufferDelegate 协议的 - (void)captureOutput AVCaptureOutput *)output didOutputSampleBuffer CMSampleBufferRef)sampleBuffer fromConnection AVCaptureConnection *)connection 方法给出,且该方法被调用的线程,就是之前我们创建的串行队列对应的线程
采集到的原始视频数据,存放在 CMSampleBuffer 中,前面的章节也提到,CMSampleBuffer 可以包含未编码的视频数据,存放在 CVPixelBuffer 中,获取 CVPixelBuffer 的代码如下
- CVPixelBufferRef pixel_buffer = CMSampleBufferGetImageBuffer(sampleBuffer);
复制代码
拿到 CVPixelBuffer 之后,视频采集的环节基本可以告一段落了,CVPixelBuffer 可以拿来做硬件编码、渲染,也可以直接把视频数据提取出来做其他的逻辑
从 CVPixelBuffer 中提取数据时需要额外注意 stride 和 width 可能差别,假如差别需要做逐行拷贝(stride 的概念可参考我们的公众号文章音视频处置惩罚必读:YUV格式详解及内存对齐本事)
- // 提取数据之前需要锁定 CVPixelBuffer
- CVPixelBufferLockBaseAddress(pixelBuffer, 0);
- size_t pixelWidth = CVPixelBufferGetWidth(pixelBuffer);
- size_t pixelHeight = CVPixelBufferGetHeight(pixelBuffer);
- unsigned long dataLength = 0;
- unsigned char* outputData = NULL;
- // 提取 NV12 数据
- dataLength = pixelWidth * pixelHeight / 2 * 3;
- outputData = (unsigned char *)malloc(dataLength);
- memset(outputData, 0, dataLength);
-
- unsigned char *yData = (unsigned char *)CVPixelBufferGetBaseAddressOfPlane(pixelBuffer, 0);
- unsigned char *uvData = (unsigned char *)CVPixelBufferGetBaseAddressOfPlane(pixelBuffer, 1);
- size_t yDataSizePerRow = CVPixelBufferGetBytesPerRowOfPlane(pixelBuffer, 0);
- size_t uvDataSizePerRow = CVPixelBufferGetBytesPerRowOfPlane(pixelBuffer, 1);
- size_t yDataSize = pixelWidth * pixelHeight;
- if (pixelWidth != yDataSizePerRow) {
- // 考虑到 pixelBuffer 中内存对齐
- // 当每行数据长度与视频宽不一致时,逐行进行数据拷贝
- for (int i = 0; i < pixelHeight; i++) {
- memcpy(outputData + pixelWidth * i, yData + yDataSizePerRow * i, pixelWidth);
- }
- for (int i = 0; i < (pixelHeight >> 1); i++) {
- memcpy(outputData + yDataSize + pixelWidth * i, uvData + uvDataSizePerRow * i, pixelWidth);
- }
- } else {
- // 直接拷贝
- memcpy(outputData, yData, yDataSize);
- memcpy(outputData + yDataSize, uvData, yDataSize >> 1);
- }
- // 数据处理完之后需要解锁 CVPixelBuffer
- CVPixelBufferUnlockBaseAddress(pixelBuffer, 0);
复制代码
写在末了
以上就是本文的所有内容了,主要介绍了如安在 iOS 平台实现摄像头的视频采集
本文为音视频底子本事系列文章的第 1 篇,后续出色内容,敬请期待
假如您以为以上内容对您有所帮助的话,可以关注下我们运营的公众号“声知视界” ,会定期的推送 音视频技术、移动端技术 为主轴的 科普类、底子知识类、行业资讯类等相关文章
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。 |