十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

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

AVFoundation实战:从自定义相机到直播推流的完整指南 第一次在项目里接到“做一个能拍短视频的功能”这个需求时我第一反应是直接拖一个UIImagePickerController上去。说实话那会儿20分钟就能拍照、录视频看起来效率很高。但产品需求一旦变成“录到12秒自动停、拍摄界面要自定义UI、直播要实时预览并推流、录制过程中要叠加时间水印”UIKit那套封装就完全撑不住了。也就是从那时起我才真正沉下心把AVFoundation啃了一遍。这篇文章想和你聊的就是AVFoundation在照片/视频捕捉上的完整落地思路。它是什么、能做什么、解决什么问题以及在小视频、直播这类实战场景里怎么选参数、怎么组织代码、又会在哪些地方踩坑。无论你是刚开始接触iOS相机开发还是已经在用UIImagePickerController但准备转向更底层的自定义相机这篇内容都能给你一条比较清晰的路线。1. AVFoundation在iOS媒体能力中的位置不是所有相机需求都得硬写底层很多人一听说“用AVFoundation做相机”第一反应是“这是不是有点重了”。这里面其实有个认知误区不是所有相机需求都得直接操作底层硬件但如果你要的是“可控”那AVFoundation基本是绕不开的中间层。1.1 先分清三套APIUIKit、AVFoundation、CoreMedia/VideoToolboxiOS的媒体能力大概分三个层级每层的控制粒度完全不同。第一层是UIKit封装的相机能力典型代表就是UIImagePickerController和PHPickerViewController。这一层适合“赶快出活”的场景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和commitConfigurationAVCaptureSession的作用是协调输入和输出之间的数据流。你往会话里添加一个输入、一个输出它就开始管理整个采集流程。但这里有个关键习惯所有配置改动要包在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、AudioDataAVFoundation输出端的类型直接决定了数据流出去之后的形态。常用的有四种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之后推荐的获取设备方式你指定设备类型广角、长焦、超广角、媒体类型视频/音频和位置前置/后置/不指定就能拿到对应的硬件对象。第二canAddInput和canAddOutput这个判断一定不能省不同机型的支持能力差异很大直接硬加会导致会话配置失败。配置完成后把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还是AVCaptureVideoDataOutputAVCaptureMovieFileOutput最大的优点是省心你只需要配置好输入输出调用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控制帧率则需要单独在设备上配置。举个例子要让后置摄像头跑60fpsfunc 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做美颜、调色等处理 // 处理完之后交给编码器或者继续传给预览 // 注意控制处理耗时避免掉帧 }这里最大的坑是性能。实时处理一帧的时间不能超过帧间隔否则采集管线会追不上现实时间表现出来就是画面卡顿、录出来的视频一卡一卡的。我的个人经验是滤镜不要堆太多能用硬件加速的就用硬件加速比如MetalCPU端的CIFilter操作要尽量精简。5. 直播场景的进阶数据回调、对焦、缩放、性能小视频和直播在采集层面最大的区别是直播要的是“源源不断的数据流”而不是“一个文件”。所以直播场景里AVCaptureVideoDataOutput和AVCaptureAudioDataOutput是绝对的主力。这一节我重点讲讲直播采集的几个核心操作。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这三角关系理顺之后后面写照片、录像、直播就都是在同一个框架里换不同的搭法。我在项目里还有一个习惯把相机整个封装成一个单独的模块对外只暴露startCamera、stopCamera、takePhoto、startRecording这类语义明确的接口不让业务层直接碰AVCaptureSession。这样一来切换镜头、调整参数、替换滤镜都只在模块内部改业务层完全不用关心底层是用的MovieFileOutput还是VideoDataOutput。如果你刚开始做这块不用急着把直播那套链路一步到位。先从拍照链路抄一遍跑通后再接视频录制最后再往数据帧方向走每一步都验证扎实了再往后推进。AVFoundation虽然上手门槛比UIKit高一些但一旦跨过这道坎后面你会突然发现原来系统相机的很多交互细节自己也能一点一点搭出来了。
返回列表