news 2026/9/16 23:47:03

AVFoundation实战:从自定义相机到直播推流的完整指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
AVFoundation实战:从自定义相机到直播推流的完整指南

第一次在项目里接到“做一个能拍短视频的功能”这个需求时,我第一反应是直接拖一个UIImagePickerController上去。说实话,那会儿20分钟就能拍照、录视频,看起来效率很高。但产品需求一旦变成“录到12秒自动停、拍摄界面要自定义UI、直播要实时预览并推流、录制过程中要叠加时间水印”,UIKit那套封装就完全撑不住了。也就是从那时起,我才真正沉下心把AVFoundation啃了一遍。

这篇文章想和你聊的,就是AVFoundation在照片/视频捕捉上的完整落地思路。它是什么、能做什么、解决什么问题,以及在小视频、直播这类实战场景里,怎么选参数、怎么组织代码、又会在哪些地方踩坑。无论你是刚开始接触iOS相机开发,还是已经在用UIImagePickerController但准备转向更底层的自定义相机,这篇内容都能给你一条比较清晰的路线。

1. AVFoundation在iOS媒体能力中的位置:不是所有相机需求都得硬写底层

很多人一听说“用AVFoundation做相机”,第一反应是“这是不是有点重了”。这里面其实有个认知误区:不是所有相机需求都得直接操作底层硬件,但如果你要的是“可控”,那AVFoundation基本是绕不开的中间层。

1.1 先分清三套API:UIKit、AVFoundation、CoreMedia/VideoToolbox

iOS的媒体能力大概分三个层级,每层的控制粒度完全不同。

第一层是UIKit封装的相机能力,典型代表就是UIImagePickerControllerPHPickerViewController。这一层适合“赶快出活”的场景:UI固定、流程固定、不需要在录制过程中做任何干预。拍照、选图、录一段简单的视频,直接调就完了。但它的边界也很明显——你不能改拍照按钮的交互样式,不能实时处理帧数据,更不可能在录制的同时做推流。

第二层就是AVFoundation,它提供的是对采集设备的会话级控制。你可以理解成UIKit帮你把“相机”封装成了一个整体,而AVFoundation把“相机”拆成了几张拼图:设备(设备型号、位置、对焦、曝光)、输入(设备怎么接入会话)、输出(采集到的数据以什么形式出去,是照片、视频文件还是裸数据帧)、会话(它们之间的协同关系)。这一层已经能覆盖绝大部分自定义相机需求,也是本文的核心。

第三层是CoreMedia和VideoToolbox,它们处理的是编码、解码、帧级别的数据操作。直播推流时你得把CMSampleBuffer喂给VideoToolbox做H.264硬编码,这已经是流媒体链路里偏后面的环节了。绝大多数App开发者不会长期泡在这一层,但得知道存在这条链路。

1.2 什么场景必须用AVFoundation

根据我自己的经验,只要踩中下面任意一条,就可以直接考虑AVFoundation:

  • 自定义拍摄界面:比如竖排按钮、全屏预览、手势滑动变焦,这些用UIImagePickerController根本做不到。
  • 录制过程中实时叠加画面:水印、贴纸、美颜效果,必须拿到实时帧数据才行,AVCaptureMovieFileOutput这类输出拿的是编码后的文件,改不了画面。
  • 直播推流:采集端和编码/推流端是两回事,采集端需要把视频帧和音频帧回调给业务层,而不是简单落成一个MP4文件。
  • 同时采集多路数据:比如前后双摄同时采,或者视频+深度数据一起采。
  • 手动控制相机参数:对焦位置、曝光补偿、ISO、快门速度、白平衡,这些在系统相机里能调的,AVFoundation都给了接口。

换句话说,你的需求越接近“做一个像系统相机一样的东西”,就越应该从AVFoundation入手。反过来说,如果只是上传一张图片,PHPicker就够用了,没必要自己再造轮子。

2. 理解AVCaptureSession的三角关系:Input、Output、Connection

说完层级,直接进入核心。AVFoundation的采集体系可以用“三角关系”来概括:AVCaptureSession是中枢,AVCaptureDeviceInput代表硬件设备的输入,AVCaptureOutput代表数据的输出,而输入和输出之间通过AVCaptureConnection建立连接。先把这个结构理解透,后面写代码就是搭积木的事。

2.1 会话是中枢:beginConfiguration和commitConfiguration

AVCaptureSession的作用是协调输入和输出之间的数据流。你往会话里添加一个输入、一个输出,它就开始管理整个采集流程。但这里有个关键习惯:所有配置改动要包在beginConfiguration()commitConfiguration()之间。

private let session = AVCaptureSession() func configureSession() { session.beginConfiguration() // 在这里做添加/移除输入输出、调整preset等操作 session.sessionPreset = .high session.commitConfiguration() }

为什么要这样?因为beginConfiguration()之后,系统会暂时挂起配置变更通知,等你把一系列操作都改完再统一提交。如果你不做这个包装,每次增删输入输出都可能触发重新配置,容易造成卡顿甚至崩溃。这是一个新手很容易忽略、但影响很大的细节。

sessionPreset用来设定采集的整体质量档位,比如AVCaptureSession.Preset.high.hd1280x720.hd1920x1080.hd4K3840x2160。需要注意的是,preset只是告诉系统“我大概要什么质量”,最终能不能达到还取决于硬件支持情况。所以严谨的做法是用session.canSetSessionPreset()做一次判断。

2.2 Connection:输入和输出之间的连接线

AVCaptureConnection是最容易被忽视、却又极其重要的概念。它描述了从一个输入设备(精确到一个指定的物理端口)到一个输出通道的连接关系。

比如后置摄像头采集到的画面,要输出给AVCaptureMovieFileOutput用于录像,同时也想输出给AVCaptureVideoPreviewLayer做预览。这两个输出各自与同一路输入建立Connection,互不干扰。而在直播场景里,你可以让视频数据输出给AVCaptureVideoDataOutput拿到帧数据做美颜处理,音频数据输出给AVCaptureAudioDataOutput单独采集,互相同步后再合流推出去。这就是Connection的灵活性所在。

另外,Connection还携带了一些关键属性,比如:videoOrientation(画面方向)、preferredVideoStabilizationMode(视频防抖模式)、isVideoMirrored(镜像,前置摄像头默认要开)。照片方向不对、视频拍出来上下颠倒,多数情况下都是这里没设好。

2.3 输出端的多样性:Photo、MovieFile、VideoData、AudioData

AVFoundation输出端的类型直接决定了数据流出去之后的形态。常用的有四种:

  • AVCapturePhotoOutput:用于静态照片采集,支持RAW、Live Photo、闪光灯控制、预览捕获等。拍一张照片时,它会通过delegate回调给你AVCapturePhoto对象,里面有完整的图片数据和元数据。
  • AVCaptureMovieFileOutput:用于直接录制视频文件,底层帮你封装好了MP4/QuickTime格式。适合做“录完就能拿文件”的普通小视频功能。
  • AVCaptureVideoDataOutput:逐帧输出原始视频数据(CMSampleBuffer),适合做实时滤镜、美颜、直播编码。这是进阶方向的核心。
  • AVCaptureAudioDataOutput:同理,逐帧输出音频数据,常用于直播或录音需要单独处理音轨的场景。

我一般会先问自己:这个功能是需要文件,还是需要帧数据?需要文件就省事用MovieFileOutput,需要帧就得上VideoDataOutput,这决定了后续的代码架构完全不同。

3. 照片捕捉的完整落地:从配置到写相册

现在开始真正动手。先做一个最小可用的“拍照功能”,把整个链路打通:配置会话 -> 预览画面 -> 按下快门 -> 拿到照片 -> 保存到相册。这一节的内容适合当作模板直接抄。

3.1 配置一个基本可用的照片采集会话

一个最基础的照片采集会话,需要三个部分:会话、输入(后置摄像头)、输出(AVCapturePhotoOutput),外加一个预览Layer。

import AVFoundation import UIKit class CameraManager: NSObject { let session = AVCaptureSession() private let photoOutput = AVCapturePhotoOutput() var previewLayer: AVCaptureVideoPreviewLayer? func configureSession() -> Bool { session.beginConfiguration() session.sessionPreset = .high // 1. 获取后置广角摄像头 guard let device = AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: .back) else { return false } // 2. 创建设备输入 guard let input = try? AVCaptureDeviceInput(device: device) else { return false } // 3. 添加输入并校验 guard session.canAddInput(input) else { return false } session.addInput(input) // 4. 添加照片输出并校验 guard session.canAddOutput(photoOutput) else { return false } session.addOutput(photoOutput) session.commitConfiguration() return true } func startSession() { if !session.isRunning { // 注意:startRunning是异步的,后面会细说 session.startRunning() } } func stopSession() { if session.isRunning { session.stopRunning() } } }

这段代码有两点值得注意。第一,AVCaptureDevice.default(_:for:position:)是iOS 10之后推荐的获取设备方式,你指定设备类型(广角、长焦、超广角)、媒体类型(视频/音频)和位置(前置/后置/不指定),就能拿到对应的硬件对象。第二,canAddInputcanAddOutput这个判断一定不能省,不同机型的支持能力差异很大,直接硬加会导致会话配置失败。

配置完成后,把previewLayer挂到界面上:

func setupPreview(in view: UIView) { let layer = AVCaptureVideoPreviewLayer(session: session) layer.videoGravity = .resizeAspectFill layer.frame = view.bounds view.layer.addSublayer(layer) previewLayer = layer }

3.2 按下快门:AVCapturePhotoOutput的完整回调链路

照片输出最有用的点是它能生成一张附带完整元数据的静态图片,而不是简单截取预览Layer的内容。按下快门时,调用capturePhoto(with:delegate:)方法,传入一个AVCapturePhotoSettings对象和一个delegate。

func takePhoto() { let settings = AVCapturePhotoSettings() // 如果设备支持闪光灯,可以在这里控制 if photoOutput.supportedFlashModes.contains(.auto) { settings.flashMode = .auto } photoOutput.capturePhoto(with: settings, delegate: self) }

然后实现AVCapturePhotoCaptureDelegate的回调方法:

extension CameraManager: AVCapturePhotoCaptureDelegate { func photoOutput(_ output: AVCapturePhotoOutput, didFinishProcessingPhoto photo: AVCapturePhoto, error: Error?) { guard error == nil, let data = photo.fileDataRepresentation() else { print("照片数据处理失败: \(String(describing: error))") return } // 拿到data后,可以做上传、做滤镜、或保存相册 saveToPhotoLibrary(data) } }

fileDataRepresentation()返回的是完整可用的图片数据,内部已经帮你处理好了HEIC/JPEG的包装逻辑。如果你直接拿photo.pixelBuffer自己去做渲染,那就是另一个更进阶的话题了。

3.3 保存到相册:PHPhotoLibrary的权限与写入

照片拿到手,接下来是保存。这里强烈建议用PHPhotoLibrary而不是直接写文件到tmp目录,因为相册才是用户能直接看到的媒介。

func saveToPhotoLibrary(_ data: Data) { PHPhotoLibrary.requestAuthorization { status in guard status == .authorized || status == .limited else { print("没有相册写入权限") return } PHPhotoLibrary.shared().performChanges { let request = PHAssetCreationRequest.forAsset() request.addResource(with: .photo, data: data, options: nil) } completionHandler: { success, error in if success { print("保存成功") } else { print("保存失败: \(String(describing: error))") } } } }

iOS 14之后新增了.limited授权状态,意味着用户只允许你访问部分照片。对于“拍照后保存到相册”这个场景,.limited其实够用,但需要在界面上给用户一个明确的能力说明。

我实际做的时候还发现一个问题:performChanges的completionHandler不一定在主线程回调,如果你要在里面刷新UI,记得先DispatchQueue.main.async包一层。

4. 视频录制与小视频场景:帧率、码率、分辨率怎么定

拍照只是第一步。小视频场景里,录制一段可预览、可分享的视频才是重点。这一节我会聊两套方案:直接用AVCaptureMovieFileOutput录文件,以及用AVCaptureVideoDataOutput获取帧数据后再做处理。两者适用的需求完全不同。

4.1 AVCaptureMovieFileOutput还是AVCaptureVideoDataOutput

AVCaptureMovieFileOutput最大的优点是省心:你只需要配置好输入输出,调用startRecording(to:recordingDelegate:),系统就开始往指定路径写文件,录制结束后delegate回调给你一个可播放的本地视频文件。

let movieOutput = AVCaptureMovieFileOutput() // 配置会话时 if session.canAddOutput(movieOutput) { session.addOutput(movieOutput) } // 开始录制 let path = NSTemporaryDirectory() + UUID().uuidString + ".mp4" movieOutput.startRecording(to: URL(fileURLWithPath: path), recordingDelegate: self)

如果只是做一个简单的“按住录制、松手出片”的功能,MovieFileOutput完全够用。它的缺点是:你拿不到原始帧数据,也就没法在录制过程中加水印、美颜、替背景。

反过来,AVCaptureVideoDataOutput会在每一帧采集完成后,把CMSampleBuffer回调给你。你可以把这一帧交给CoreImage做滤镜,或者交给VideoToolbox做硬编码,再封装成推流格式。这也是直播采集端的标准姿势。

4.2 小视频场景下的参数选择:不是越高越好

刚接触视频采集时,大家的常见问题是“能上4K就上4K,能60帧就60帧”。但实际做小视频功能时,这个思路会带来两个麻烦:一是文件体积大得离谱,15秒的4K视频动不动上百MB;二是弱光环境下的高帧率高分辨率画质反而更差。

我的建议是:

场景推荐分辨率推荐帧率备注
15秒短视频分享720p或1080p30fps体积和画质平衡
慢动作特效720p120fps或240fps依赖设备能力和光线
直播推流720p或1080p30fps码率受网络限制
高画质短视频1080p60fps适合运动画面

分辨率通过sessionPreset控制,帧率则需要单独在设备上配置。举个例子,要让后置摄像头跑60fps:

func configureHighFrameRate() { guard let device = AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: .back) else { return } var selectedFormat: AVCaptureDevice.Format? var selectedFPS: Int = 30 for format in device.formats { for range in format.videoSupportedFrameRateRanges { if range.maxFrameRate >= 60 && range.minFrameRate <= 30 { selectedFormat = format selectedFPS = Int(min(range.maxFrameRate, 60)) } } } guard let format = selectedFormat else { return } do { try device.lockForConfiguration() device.activeFormat = format device.activeVideoMinFrameDuration = CMTime(value: 1, timescale: CMTimeScale(selectedFPS)) device.activeVideoMaxFrameDuration = CMTime(value: 1, timescale: CMTimeScale(selectedFPS)) device.unlockForConfiguration() } catch { print("配置高帧率失败: \(error)") } }

这里的逻辑是:遍历设备支持的所有格式,找到能跑60fps的格式,然后通过activeFormat切换过去。为什么要遍历?因为不是所有分辨率都支持高帧率,直接写死格式编号是一个非常脆弱的做法。

另外,高帧率模式会显著增加功耗和发热,录制超过几分钟后,系统可能会因为thermal压力自动降频甚至中断会话。所以给小视频功能定帧率时,千万别盲目追求120fps。

4.3 录制中做特效:用VideoDataOutput接CoreImage

如果你需要在录制过程中实时叠加滤镜或水印,方案就不是MovieFileOutput,而是AVCaptureVideoDataOutput+ CoreImage的组合。

大致流程是这样:videoDataOutput的代理方法captureOutput(_:didOutput:from:)里拿到每个样本缓冲,转换成CIImage后做滤镜处理,再交给后续编码器。这就意味着采集链路变成了“采集 -> 处理 -> 编码 -> 封装”,每一步都需要你自己掌控。

func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) { guard let pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) else { return } let ciImage = CIImage(cvPixelBuffer: pixelBuffer) // 此时可以用CIFilter做美颜、调色等处理 // 处理完之后交给编码器或者继续传给预览 // 注意控制处理耗时,避免掉帧 }

这里最大的坑是性能。实时处理一帧的时间不能超过帧间隔,否则采集管线会追不上现实时间,表现出来就是画面卡顿、录出来的视频一卡一卡的。我的个人经验是:滤镜不要堆太多,能用硬件加速的就用硬件加速(比如Metal),CPU端的CIFilter操作要尽量精简。

5. 直播场景的进阶:数据回调、对焦、缩放、性能

小视频和直播在采集层面最大的区别,是直播要的是“源源不断的数据流”而不是“一个文件”。所以直播场景里,AVCaptureVideoDataOutputAVCaptureAudioDataOutput是绝对的主力。这一节我重点讲讲直播采集的几个核心操作。

5.1 直播采集链路:CMSampleBuffer交给VideoToolbox硬编

直播的完整链路是:摄像头/麦克风采集原始数据 -> 对视频帧做美颜/滤镜 -> 编码器(一般用VideoToolbox的H.264硬编) -> 封装成FLV等格式 -> 推流到CDN。

在AVFoundation这一层,你要做的事就是配置一个同时包含视频和音频输出的会话,然后在回调里把样本数据按时间戳交给编码器。

session.beginConfiguration() // 添加视频输入 // 添加音频输入 let videoOutput = AVCaptureVideoDataOutput() videoOutput.videoSettings = [ kCVPixelBufferPixelFormatTypeKey as String: kCVPixelFormatType_32BGRA ] session.addOutput(videoOutput) let audioOutput = AVCaptureAudioDataOutput() session.addOutput(audioOutput) session.commitConfiguration()

kCVPixelBufferPixelFormatTypeKey这里为什么要设成32BGRA?因为VideoToolbox的H.264编码器通常接收BGRA格式的像素缓存,CoreImage也能直接处理这个格式。如果你不指定,系统给的默认格式可能需要在编码前多一次转换,白白浪费性能。

直播场景另一个麻烦是音视频同步。两个独立的数据输出回调在不同的时间到达,一定要靠时间戳对齐。CMSampleBuffer里自带presertationTimeStamp,编码时按这个时间戳打上PTS,就不会错位。

5.2 点按对焦与曝光:手动控制的核心方法

做直播一个非常高频的交互是“点按画面中对焦”。AVFoundation里对焦和曝光都是用设备对象来控制的。

func focusAndExpose(at point: CGPoint) { guard let device = AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: .back) else { return } do { try device.lockForConfiguration() if device.isFocusPointOfInterestSupported { device.focusPointOfInterest = point device.focusMode = .autoFocus } if device.isExposurePointOfInterestSupported { device.exposurePointOfInterest = point device.exposureMode = .autoExpose } device.unlockForConfiguration() } catch { print("设置对焦/曝光失败: \(error)") } }

注意,在调用lockForConfiguration()之后一定要在合适的时机unlockForConfiguration()。如果忘记解锁,后续其他操作会一直失败,而且系统日志里不会直接报错,排查起来很难受。

对焦模式有.locked.autoFocus.continuousAutoFocus几种。直播场景里通常用.autoFocus让用户点一下后自动对焦到这个点,如果镜头前的东西移动了再重新点击。连续自动对焦适合拍摄移动物体,但会带来画面轻微呼吸感,直播时反而容易显得不稳定。

曝光也一样,有.locked.autoExpose.continuousAutoExposure。如果需要手动调亮度,可以进一步设置device.exposureTargetBias

5.3 变焦与视频稳定

直播画面里“双指缩放”也是高频操作。AVFoundation的变焦通过videoZoomFactor控制,取值范围在1.0和activeFormat.videoMaxZoomFactor之间。

func setZoom(factor: CGFloat) { guard let device = AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: .back) else { return } do { try device.lockForConfiguration() device.videoZoomFactor = max(1.0, min(factor, device.activeFormat.videoMaxZoomFactor)) device.unlockForConfiguration() } catch { print("设置变焦失败: \(error)") } }

videoMaxZoomFactor在不同机型、不同格式下差异很大,有些长焦镜头的最大值甚至超过10倍。你直接写死8.0在低端设备上可能就崩溃或越界,所以一定要做min/max夹取。

视频防抖方面,AVCaptureConnection上有一个preferredVideoStabilizationMode属性,可选.off.standard.cinematic.auto。直播场景通常建议用.auto,让系统根据当前光线和运动幅度决定是否开启防抖。防抖会带来画面裁切(因为需要给移动留出余量),如果你发现直播画面怎么比别的App少了一块,多半是防抖模式导致的。

5.4 性能监控与掉帧排查

直播掉帧是最让人头疼的问题,因为采集端的掉帧会被直接放大成观众端的卡顿。排查时我一般分三步:先看CPU占用率,再看GPU占用率(通过Instruments里的Core Animation调试),最后看是否有thermal压力。

在代码里可以挂一个简单的帧间隔统计:

var lastTimestamp: CMTime? var frameCountSinceLog: Int = 0 func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) { if let last = lastTimestamp { let interval = CMTimeSubtract(sampleBuffer.presentationTimeStamp, last) if CMTimeCompare(interval, CMTime(value: 1, timescale: 30)) > 0 { // 当前帧间隔大于33ms,说明可能掉帧了 } } lastTimestamp = sampleBuffer.presentationTimeStamp }

如果掉帧,优先检查三件事:一是处理帧数据的回调里有没有做耗时操作,比如直接在主线程跑CoreImage;二是videoDataOutput有没有设置automaticallyDropsLateVideoFrames,这个属性在实时处理场景默认值为true,如果处理跟不上,系统会主动丢帧,这是保护机制,一般不用关;三是推流端的发送缓冲区是否堆积,发送不过去时整个采集会话会受影响。

6. 真实项目里的坑和排查思路:session为什么会黑屏

最后用一节的篇幅,把我在实际项目里踩得最深的几个坑整理出来。这些问题在网上讨论很多,但每次自己遇到时还是会绕好几个弯。如果你在看这篇内容之前就已经动过手,大概率也踩过其中一两个。

6.1 黑屏问题:session.startRunning()其实是异步的

刚接触AVFoundation时,我遇到的第一类问题就是“明明配置好了会话,界面上一片黑”。原因是startRunning()是一个异步操作,它不会马上点亮摄像头,而是要等系统真正启动采集链路后,画面才会出现。如果在startRunning()返回后就立刻把预览Layer显示出来,很可能看到的是黑屏。

处理方式有两种:一是通过KVO监听session.isRunning,等它变成true后再显示预览;二是注册AVCaptureSession.runtimeErrorNotification,捕获采集过程中的运行时错误。最稳妥的做法是两者结合,直播App里尤其要监听runtimeError,因为直播时摄像头被其他App抢占、或者设备发热导致采集被中断,都会通过这个通知暴露出来。

我也见过“黑屏但session还是running”的情况。这时通常是因为预览Layer没有正确关联到session,或者videoGravity设得不对导致画面渲染区域异常。检查时先确认previewLayer是否还在视图层级里,再检查frame是否有效。

6.2 前后台切换与权限变更后的重启

App退到后台再接回来,经常发现相机黑屏或者摄像头打不开。原因是退后台时系统会暂停相机硬件,但你的session可能还处于running状态;回到前台时,硬件恢复需要重新配置。

我的习惯是在App进入后台时调用session.stopRunning(),进入前台后再session.startRunning()。但这又引出另一个坑:startRunning()是阻塞方法,如果在主线程调用而设备忙于配置,可能会导致UI卡顿,严重时还会被看门狗杀掉。所以更规范的做法是把startRunning()放到一个串行队列里,而不是直接在applicationDidBecomeActive里调用。

权限方面,如果用户进App时没授权相机,后面在设置里打开权限回到App,系统不会自动把session重新跑起来,需要在回调里重新配置并startRunning()

6.3 高帧率不可用:前置摄像头的格式限制

做美颜直播时,有一个高频现象:后置摄像头跑60fps没问题,切到前置摄像头就提示“不支持”。这不是代码写错了,而是设备本身对前置摄像头的格式支持范围更窄。

解决方式也是遍历device.formats检查videoSupportedFrameRateRanges,在切换镜头前先判断目标设备是否支持当前设置的fps和分辨率。如果支持就正常切换,不支持就回退到30fps并给用户一个提示。这个过程需要在切镜头时同步更新activeFormat,否则即便session里有新input,帧率和分辨率还是老一套。

6.4 保存视频/照片失败:PHPhotoLibrary权限与存储空间

最后说一个“项目快要上线了才暴露”的问题:用户拒绝相册权限时,很多代码直接崩溃或者静默失败。iOS 14之后的授权状态更细,requestAuthorization回调里除了.authorized.denied,还有.limited。如果你的App只是把用户拍摄的内容保存到相册,.limited其实够用,但要给用户一个“只允许部分照片”的引导说明。

另一个容易被忽略的是存储空间判断。视频录制时如果存储满了,AVCaptureFileOutputRecordingDelegate的回调会带一个error,代码里如果只判断“文件是否存在”而不判断error,就会以为录制成功了,其实文件没写完整。所以我通常会这样写:

func fileOutput(_ output: AVCaptureFileOutput, didFinishRecordingTo outputFileURL: URL, from connections: [AVCaptureConnection], error: Error?) { if let error = error { // 处理录制中断,清理临时文件 return } // 正常处理 outputFileURL }

有时录制出问题的是用户还在返回上一页,录制会话已经停止了,回调里依然会走didFinishRecordingTo,这个时候error可能是nil,但文件时长很短。判断时最好再检查一下文件的实际时长和大小,避免保存一个0字节的“成功文件”。

最后一点实操体会

AVFoundation这套框架,刚入手时觉得类多、调用繁琐,但当你把Input、Output、Connection这三角关系理顺之后,后面写照片、录像、直播就都是在同一个框架里换不同的搭法。

我在项目里还有一个习惯:把相机整个封装成一个单独的模块,对外只暴露startCamerastopCameratakePhotostartRecording这类语义明确的接口,不让业务层直接碰AVCaptureSession。这样一来,切换镜头、调整参数、替换滤镜都只在模块内部改,业务层完全不用关心底层是用的MovieFileOutput还是VideoDataOutput

如果你刚开始做这块,不用急着把直播那套链路一步到位。先从拍照链路抄一遍,跑通后再接视频录制,最后再往数据帧方向走,每一步都验证扎实了再往后推进。AVFoundation虽然上手门槛比UIKit高一些,但一旦跨过这道坎,后面你会突然发现:原来系统相机的很多交互细节,自己也能一点一点搭出来了。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/16 23:46:02

麒麟V10 ARM平台部署达梦数据库DM8实战指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/16 23:45:59

NAS、SAN、ISCSI到底啥区别?从文件级到块级存储的选型指南

打开购物网站搜“NAS”&#xff0c;能看到几百块的玩客云&#xff0c;也能看到上万块的群晖企业级&#xff1b;搜“SAN”&#xff0c;出来的基本是机柜、光纤线和一个让人不敢追问的价格&#xff1b;再搜“ISCSI”&#xff0c;大多文章写得太硬核&#xff0c;读三行就劝退。很多…

作者头像 李华
网站建设 2026/9/16 23:44:57

WinCC V7.5 SP1安装与服务器授权选型实战指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/16 23:44:26

eNSP三层交换机VLANIF配置指南:跨VLAN通信原理与排错实战

用eNSP做实验的时候&#xff0c;很多人都会卡在同一个地方&#xff1a;VLAN划分得好好的&#xff0c;接口也都加进去了&#xff0c;可不同VLAN里的PC就是ping不通。这时候绕不开一个概念——VLANIF。它既是三层交换机上给VLAN提供的“网关接口”&#xff0c;也是跨VLAN通信的必…

作者头像 李华
网站建设 2026/9/16 23:41:12

Dify 1.9 知识库流水线:图像与表格数据解析实战与踩坑指南

Dify 的知识库功能我一直觉得是最能体现“工程化”价值的模块。文本文件往里一丢就能跑起来&#xff0c;体验相当丝滑&#xff0c;但一旦碰到表格、扫描件、截图这类非纯文本数据&#xff0c;效果立刻打折扣。最近我基于 Dify 1.9 的知识库流水线模板&#xff0c;专门把图像和表…

作者头像 李华