2、AVFoundation框架入门:核心类、权限与会话配置

好,咱们正式开始接触音视频开发的核心——AVFoundation。说实话,这个框架是iOS上做音视频处理绕不开的坎。我当年刚接触时,也被它庞大的类体系搞得晕头转向。但别怕,今天咱们就把最核心的几个类、权限申请和会话配置讲透。

2.1 AVFoundation核心类介绍

AVFoundation里类很多,但真正天天打交道的,其实就这几个。我个人习惯把它们分成三组:

2.1.1 设备相关:AVCaptureDevice

这个类代表物理硬件设备。摄像头、麦克风,都是它。你可以通过它查询设备是否可用、支持哪些格式、当前对焦状态等。

// 获取后置摄像头
guard let backCamera = AVCaptureDevice.default(.builtInWideAngleCamera, 
                                                for: .video, 
                                                position: .back) else {
    print("后置摄像头不可用")
    return
}

嗯,这里要注意:.builtInWideAngleCamera是iOS 10之后才有的写法。老项目里可能还在用AVCaptureDevice.devices(),但那个已经废弃了。我在接手一个遗留项目时,就踩过这个坑——编译没问题,跑在iOS 14上直接崩溃。

2.1.2 输入输出:AVCaptureInput 与 AVCaptureOutput

说白了,输入就是数据从哪里来,输出就是数据到哪里去。

类名 作用 常见子类
AVCaptureInput 数据源,连接设备 AVCaptureDeviceInput
AVCaptureOutput 数据目的地 AVCaptureVideoDataOutput、AVCaptureAudioDataOutput、AVCaptureMovieFileOutput

你想想看,摄像头采集到的原始数据,通过AVCaptureDeviceInput进入系统,然后你可以选择:

  • AVCaptureVideoDataOutput拿到每一帧的像素数据,自己做美颜或滤镜
  • AVCaptureMovieFileOutput直接录制成文件,省心省力

2.1.3 会话管理:AVCaptureSession

这个是最关键的。它就像一个大管家,负责把输入和输出连接起来,管理整个采集流程。

let session = AVCaptureSession()
session.sessionPreset = .high

// 添加输入
guard let input = try? AVCaptureDeviceInput(device: backCamera) else { return }
if session.canAddInput(input) {
    session.addInput(input)
}

// 添加输出
let videoOutput = AVCaptureVideoDataOutput()
if session.canAddOutput(videoOutput) {
    session.addOutput(videoOutput)
}

// 启动会话
session.startRunning()

为什么说它重要?因为所有配置变更,都要通过它来协调。我曾经遇到过一个bug:在录制过程中切换摄像头,画面卡住不动。后来发现是没在beginConfigurationcommitConfiguration之间做操作。嗯,这个后面会细讲。

2.2 权限申请与配置

iOS对隐私保护非常严格。摄像头和麦克风权限,必须用户明确授权。我刚开始做直播时,就因为没有处理好权限,被App Store拒了两次。

2.2.1 Info.plist 配置

首先,你必须在Info.plist里添加描述文字。否则调用API时直接闪退,连弹窗都没有。

<key>NSCameraUsageDescription</key>
<string>我们需要使用您的摄像头来进行直播</string>
<key>NSMicrophoneUsageDescription</key>
<string>我们需要使用您的麦克风来录制声音</string>

这里有个小细节:描述文字要写清楚用途。苹果审核时会看这个,如果写得含糊不清,可能会被驳回。

2.2.2 运行时权限申请

配置好plist后,代码里还要主动申请。我个人习惯在进入直播页面时统一申请,而不是用到时才弹窗——用户体验会好很多。

import AVFoundation

// 申请摄像头权限
AVCaptureDevice.requestAccess(for: .video) { granted in
    DispatchQueue.main.async {
        if granted {
            print("摄像头权限已获取")
        } else {
            print("用户拒绝了摄像头权限")
        }
    }
}

// 申请麦克风权限
AVCaptureDevice.requestAccess(for: .audio) { granted in
    // 类似处理
}

你可能会问:如果用户第一次拒绝了怎么办?嗯,这时候可以引导用户去设置里手动开启。但注意,不能强制弹窗,苹果不允许。

避坑指南:我曾经在申请权限时,直接在后台线程调用了UI更新,结果崩溃了。记住,权限回调不一定在主线程,一定要用DispatchQueue.main.async切回主线程再操作UI。

2.3 音视频会话配置

权限拿到后,就该配置会话了。这里涉及两个核心概念:AVCaptureSessionPresetAVCaptureConnection

2.3.1 预设质量等级

sessionPreset决定了采集的分辨率和帧率。常用的有:

预设值 分辨率 适用场景
.high 最高支持 通用录制
.medium 480p左右 网络直播
.low 360p左右 低带宽场景
.hd1920x1080 1080p 高清录制

但要注意:不是所有设备都支持所有预设。比如iPhone 6就不支持4K。我建议先查询设备支持列表:

let device = AVCaptureDevice.default(for: .video)!
if device.supportsSessionPreset(.hd1920x1080) {
    session.sessionPreset = .hd1920x1080
} else {
    session.sessionPreset = .high
}

2.3.2 配置输入输出连接

当你添加了输入和输出后,系统会自动建立连接(AVCaptureConnection)。但有时候你需要手动调整,比如设置视频方向。

// 获取视频输出连接
guard let connection = videoOutput.connection(with: .video) else { return }

// 设置视频方向为竖屏
if connection.isVideoOrientationSupported {
    connection.videoOrientation = .portrait
}

// 设置视频镜像(前置摄像头需要)
if connection.isVideoMirroringSupported {
    connection.isVideoMirrored = true
}

这里有个坑:connection(with: .video)可能返回nil。为什么?因为输出还没添加到session,或者session还没开始运行。我建议在session.startRunning()之后,或者至少确保输出已经被添加了,再获取连接。

2.3.3 动态调整配置

有时候你需要在运行中调整配置,比如切换摄像头。这时候要用beginConfigurationcommitConfiguration包裹起来。

session.beginConfiguration()

// 移除旧输入
session.removeInput(oldInput)

// 添加新输入
session.addInput(newInput)

// 提交配置
session.commitConfiguration()
个人经验:beginConfigurationcommitConfiguration之间,可以批量修改配置。系统会等所有修改完成后,一次性应用。这样能避免中间状态导致的卡顿或崩溃。

2.4 实战:搭建一个最小采集系统

好了,理论讲完了。咱们动手搭一个最简单的摄像头采集系统。把上面讲的知识点串起来:

import AVFoundation

class CameraManager {
    let session = AVCaptureSession()
    private var videoInput: AVCaptureDeviceInput?
    private var videoOutput: AVCaptureVideoDataOutput?
    
    func setupCamera() {
        // 1. 获取设备
        guard let camera = AVCaptureDevice.default(.builtInWideAngleCamera,
                                                   for: .video,
                                                   position: .back) else {
            print("摄像头不可用")
            return
        }
        
        // 2. 创建输入
        guard let input = try? AVCaptureDeviceInput(device: camera) else {
            print("创建输入失败")
            return
        }
        videoInput = input
        
        // 3. 创建输出
        let output = AVCaptureVideoDataOutput()
        output.setSampleBufferDelegate(self, queue: DispatchQueue(label: "video.queue"))
        videoOutput = output
        
        // 4. 配置会话
        session.beginConfiguration()
        session.sessionPreset = .high
        
        if session.canAddInput(input) {
            session.addInput(input)
        }
        if session.canAddOutput(output) {
            session.addOutput(output)
        }
        
        // 5. 设置连接属性
        if let connection = output.connection(with: .video) {
            if connection.isVideoOrientationSupported {
                connection.videoOrientation = .portrait
            }
        }
        
        session.commitConfiguration()
        
        // 6. 启动
        session.startRunning()
    }
}

这段代码虽然简单,但涵盖了核心流程。你把它跑起来,就能看到摄像头开始工作了。当然,要显示画面还需要加一个AVCaptureVideoPreviewLayer,这个我们下一章再讲。

总结一下今天的内容:
  • AVCaptureDevice:代表硬件设备
  • AVCaptureInput/Output:数据的输入输出
  • AVCaptureSession:核心管理者
  • 权限申请:Info.plist + requestAccess
  • 会话配置:preset、connection、begin/commitConfiguration

嗯,今天就到这里。下一章我们会深入AVCaptureVideoDataOutput,教你如何拿到每一帧图像数据,并做实时处理。到时候咱们再聊美颜和滤镜的实现思路。