首页 > 其他分享 >音视频基础能力之 iOS 视频篇(一):视频采集

音视频基础能力之 iOS 视频篇(一):视频采集

时间:2024-11-15 22:45:25浏览次数:3  
标签:视频 self iOS 音视频 采集 device output AVCaptureSession

涉及硬件的音视频能力,比如采集、渲染、硬件编码、硬件解码,通常是与客户端操作系统强相关的,就算是跨平台的多媒体框架也必须使用平台原生语言的模块来支持这些功能

本系列文章将详细讲述移动端音视频的采集、渲染、硬件编码、硬件解码这些涉及硬件的能力该如何实现

本文为该系列文章的第 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 篇,后续精彩内容,敬请期待

如果您觉得以上内容对您有所帮助的话,可以关注下我们运营的公众号“声知视界” ,会定期的推送 音视频技术、移动端技术 为主轴的 科普类、基础知识类、行业资讯类等相关文章

标签:视频,self,iOS,音视频,采集,device,output,AVCaptureSession
From: https://blog.csdn.net/AVExplorer/article/details/143808326

相关文章

  • 视频编码基础入门
    文章目录前言一、视频编码的目标二、视频编码基本流程1.采样与颜色空间转换2.变换编码(例如DCT)3.量化4.熵编码5.运动补偿和帧间预测6.编码输出三、视频编码的关键技术1.帧类型2.GOP(GroupofPictures)结构3.比特率控制四、常见的视频编码标准H.264(AVC)H.265......
  • 基于python+django的Hadoop的短视频数据分析的设计与实现
    前言基于python+django的Hadoop短视频数据分析系统可充分挖掘短视频数据价值。从各大短视频平台接口等多种数据源采集数据,利用Hadoop分布式存储海量短视频的基本信息、用户信息、播放量、点赞数、评论内容等。借助python数据分析库和django框架,清洗、预处理......
  • HarmonyOS和OpenHarmony区别是什么?鸿蒙和安卓IOS的区别是什么?
    HarmonyOS和OpenHarmony的区别简单来说:OpenHarmony开源鸿蒙-系统底座设备开发(硬件方面)应用开发(软件方面)HarmonyOS华为鸿蒙-在系统底座的基础上,添加华为各种服务:如华为登录、华为地图、华为分享、华为推送等...OpenHarmony(开源鸿蒙):它侧重于作为系统底座发挥作用......
  • Android15音频进阶之input调节CarAudioService音量过程(九十四)
    简介:CSDN博客专家、《Android系统多媒体进阶实战》一书作者新书发布:《Android系统多媒体进阶实战》......
  • 杭州市公安局打造“清风明月说反诈”系列视频,守好市民“钱袋子”
    为进一步加强辖区群众的反诈防骗意识,提升反诈宣传效率。杭州市公安局推出“清风明月说反诈”系栏目,借助有言的AIGC视频生成能力,高效率制作多场景的反诈宣传视频,升级传统视频制作方式,展示了AI技术在反诈宣传科普以及公共安全社会治理中的巨大价值。没有使用AI视频工具之前,传统......
  • 基于米尔NXP i.MX93开发板OpenCV的相机捕捉视频进行人脸检测
    本篇测评由优秀测评者“eefocus_3914144”提供。 本文将介绍基于米尔电子MYD-LMX93开发板(米尔基于NXPi.MX93开发板)的基于OpenCV的人脸检测方案测试。OpenCV提供了一个非常简单的接口,用于相机捕捉一个视频(我用的电脑内置摄像头)1、安装python3-opencvaptinstallpython3-......
  • Axios 拦截器示例(JWT 登录与自动刷新)
    1.安装axios首先,确保你已经安装了axios:npminstallaxios2.设置Axios拦截器importaxiosfrom'axios';//创建一个axios实例constaxiosInstance=axios.create({baseURL:'http://localhost:8000/',//后端API地址timeout:10000,//设置超时时间......
  • NVR接入录像回放平台EasyCVR视频设备轨迹回放平台分发webrtc流地址无法播放是什么原因
    在现代安防领域,视频监控技术扮演着越来越重要的角色。EasyCVR视频汇聚平台以其卓越的兼容性和灵活性,为用户提供了一个强大的视频监控解决方案。该平台不仅能够满足基本的视频监控需求,还通过一系列高级功能,如视频转码、快照、告警处理等,极大地提升了监控系统的智能化水平和操作便捷......
  • 无插件H5播放器EasyPlayer.js网页web无插件播放器选择全屏时,视频区域并没有全屏问题的
    EasyPlayer.jsH5播放器,是一款能够同时支持HTTP、HTTP-FLV、HLS(m3u8)、WS、WEBRTC、FMP4视频直播与视频点播等多种协议,支持H.264、H.265、AAC、G711A、MP3等多种音视频编码格式,支持MSE、WASM、WebCodec等多种解码方式,支持Windows、Linux、Android、iOS全平台终端的H5播放器,使用简单......
  • 网页直播/点播播放器EasyPlayer.js RTSP播放器出现多路视频卡顿、内存开始飙升的原因
    EasyPlayer.jsRTSP播放器是TSINGSEE青犀流媒体组件系列中关注度较高的产品,经过多年的发展和迭代,目前已经有多个应用版本,包括RTSP版、RTMP版、Pro版以及js版,其中js版本作为网页播放器,受到了用户的广泛使用。1、问题说明在已经使用硬解码基础上,播放多路视频,会出现卡顿,内存开始飙......