本文首发于小专栏WWDC23 10105 – 打造呼应更快的相机体会

摘要:iOS 17 供给了一些新的特性,经过推迟图片处理、快门零推迟、呼应捕获等新特性,以及状况监听等办法,能大幅进步相机呼应速度,发明更流通的摄影体会。

WWDC23 10105 - 打造响应更快的相机体验

背景

iOS 13 开始,经过设置 AVCapturePhotoQualityPrioritization 能够调整获取相片的质量和速度,假如咱们希望获取质量最高的相片,能够设置为 .quality

// Constants indicating how photo quality should be prioritized against speed.
@available(iOS 13.0, *)
public enum QualityPrioritization : Int, @unchecked Sendable {
    case speed = 1
    case balanced = 2
    case quality = 3 
}

.balanced.quality 这两个选项,会对相片进行多帧融合和降噪处理,在 iPhone 11 Pro 及更新的型号上会使用一项最新的技能叫 Deep Fusion,这一切的处理都需求耗时,而且必须鄙人一次摄影之前处理完结,也便是说假如未处理完结,下一次的摄影将无法实在履行。

简略解释下 Deep Fusion: Deep Fusion 是苹果在 iPhone 11 系列引进的一项图画处理技能。它利用人工智能和机器学习,将多张不同曝光度和焦距的相片融合在一起,发生一张高质量的合成相片。

这就导致了想要高质量的相片,必须抛弃速度,乃至需求更长期的等候。 而 iOS 17 新增了 API,让咱们能够确保相片质量(即 .quality)的一起,下降摄影距离时间,加速摄影呼应速度,获取更多高质量相片。

Deferred Photo Processing 推迟相片处理

原有的摄影流程

WWDC23 10105 - 打造响应更快的相机体验

首要咱们整理下摄影的全体流程,点击摄影后,调用体系的摄影办法 open func capturePhoto(with settings: AVCapturePhotoSettings, delegate: AVCapturePhotoCaptureDelegate)

咱们顺次收到署理回调 Monitor Capture Progress -> Receiving Capture Results,直到终究的 didFinishProcessingPhoto,才生成了可用的相片。

这期间,哪怕再次调用了摄影办法,也不会收效,只要等候上一张相片处理完结,才会进行下一轮的相片摄影 + 相片处理。

优化后的流程

iOS 17 之后,咱们能够敞开推迟相片处理,全体的时间线都会缩短。比照看下作用:

WWDC23 10105 - 打造响应更快的相机体验
WWDC23 10105 - 打造响应更快的相机体验

体系新增了一个 Proxy Photo 暂时相片的概念,首要作用是加速相机摄影速度和推迟实在相片处理。

经过生成 Proxy Photo,相机无需等候高质量相片处理完结就能够进行下一张相片的摄影,大大缩短了摄影距离,进步了相机捕获速度。 除速度外,Proxy Photo 还具有供给预览和暂时占位的辅佐作用。它能够让用户提早看到摄影内容,也能够暂时占位并存储在相册中,等候实在相片到来,让用户在终究结果到达前有所参考。

当咱们点击摄影,调用体系办法后,条件契合下咱们会提早收到一个新的署理办法 didFinishCapturingDeferredPhotoProxy,经过该署理咱们能够获取前面说到的 Proxy Photo,这时分就能够开始下一轮的摄影,无需等候相片处理流程。

WWDC23 10105 - 打造响应更快的相机体验
终究的相片处理流程会在相时机话完毕之后由体系来主动处理,一般是在两个时间点:

  1. 你从相册中恳求相片时按需处理,或许会导致恳求相片时耗时添加。
  2. 体系自己觉得条件契合就在后台进行处理,比如体系空闲时。

代码完结

首要需求启动推迟相片处理,启动后,条件允许下,咱们会收到新的署理办法,didFinishCapturingDeferredPhotoProxy 会替代之前的 didFinishProcessingPhoto, 经过 Proxy 目标能够拿到推迟处理的相片数据。 拿到数据后主张马上经过 PHAssetCreationRequest 存储到相册中,这样能够最大程度削减数据丢失的危险,一起也能够让体系相册尽快在后台处理并生成终究的相片。

注:有些内容是经过官方文档获取的,或许跟视频有点点出入 Add the in-memory proxy file data representation to the photo library as quickly as possible after this call to ensure that the photo library can begin background processing. It’s also important so that the intermediates aren’t removed by a periodic clean-up job looking for abandoned intermediates produced by using the deferred photo processing APIs.


private let sessionQueue = DispatchQueue(label: "sessionQueue", qos: .userInteractive)
private let session: AVCaptureSession = AVCaptureSession()
private let photoOutput: AVCapturePhotoOutput = AVCapturePhotoOutput()
private func configureCameraView() {
    contentView.videoPreviewLayer.session = session
    contentView.videoPreviewLayer.videoGravity = .resizeAspectFill
    contentView.videoPreviewLayer.connection?.videoOrientation = .portrait
    sessionQueue.async {
        self.session.beginConfiguration()
        self.session.sessionPreset = .photo
        self.addInputVideo()
        self.addPhotoOutput()
        self.session.commitConfiguration()
        // 启动相机
        self.startRunning()
    }
}
private func addInputVideo() {
    if let backCamera = defaultCameraDevice() {
        try? backCamera.lockForConfiguration()
        if backCamera.isLowLightBoostEnabled,
           !backCamera.automaticallyEnablesLowLightBoostWhenAvailable {
            backCamera.automaticallyEnablesLowLightBoostWhenAvailable = true
        }
        if backCamera.isSmoothAutoFocusSupported {
            backCamera.isSmoothAutoFocusEnabled = true
        }
        if backCamera.isFocusModeSupported(.continuousAutoFocus) {
            backCamera.focusMode = .continuousAutoFocus
        }
        backCamera.unlockForConfiguration()
        guard let videoInput = try? AVCaptureDeviceInput(device: backCamera),
              session.canAddInput(videoInput),
              !self.session.inputs.contains(videoInput) else { return }
        session.addInput(videoInput)
    }
}
private func addPhotoOutput() {
    photoOutput.maxPhotoQualityPrioritization = .quality
    photoOutput.isHighResolutionCaptureEnabled = true
    if #available(iOS 17.0, *) {
        // 敞开推迟处理
        if photoOutput.isAutoDeferredPhotoDeliverySupported {
            photoOutput.isAutoDeferredPhotoDeliveryEnabled = true
        }
    } else { }
    guard !session.outputs.contains(photoOutput),
          session.canAddOutput(photoOutput) else { return }
    session.addOutput(photoOutput)
}
private func defaultCameraDevice() -> AVCaptureDevice? {
    if let device = AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: .back) {
        return device // 0.5 - 0.6s,默许广角
    }
    return nil
}

经过署理拿到 proxy Photo,即可开始新一轮的摄影

@available(iOS 17.0, *)
func photoOutput(_ output: AVCapturePhotoOutput, didFinishCapturingDeferredPhotoProxy deferredPhotoProxy: AVCaptureDeferredPhotoProxy?, error: Error?) {
    if let error {
        print("deferredPhotoProxy error: \(error)")
        return
    }
    guard let data = deferredPhotoProxy?.fileDataRepresentation() else { return }
    let library = PHPhotoLibrary.shared()
    library.performChanges {
        let request = PHAssetCreationRequest.forAsset()
        request.addResource(with: .photoProxy, data: data, options: nil)
    } completionHandler: { _, error in
        if let error {
            print("PHAssetCreationRequest error: \(error)")
        } else {
            print("save success")
        }
    }
}

咱们能够经过现有的办法获取相册中的 Proxy Photo,前提是要设置 option allowSecondaryDegradedImage 特点,

func requestAndHandleImageRepresentations(asset: PHAsset,
                                          targetSize: CGSize,
                                          contentMode: PHImageContentMode) -> PHImageRequestID {
    let imageManager = PHImageManager.default()
    let option = PHImageRequestOptions()
    if #available(iOS 17, *) {
        option.allowSecondaryDegradedImage = true
    } else { }
    let requestID = imageManager.requestImage(for: asset, targetSize: targetSize, contentMode: contentMode, options: option) { _, info in
        if let info,
           let intValue = info[PHImageResultIsDegradedKey] as? Int,
           intValue == 1 {
            // 回调两次,一次 Initial 小图
            // 一次 Secondary 相片
        } else {
            // 回调一次
            // image 为 final 图,实在高质量大图
        }
    }
    return requestID
}

特别留意:该办法默许是异步的,resultHandler block more than once,经过 PHImageResultsDegradedKey来判别回来的是暂时相片仍是终究的高质量相片,更多详细信息看这儿

终究拿到的相片如下: Initial 是最小的相片,一般是 120 * 120 New Secondary 是新版别下 Proxy Photo 对应的相片,分辨率跟终究的相片共同 Final 终究生成的高质量的明晰大图

假如不设置 PHImageResultsDegradedKey,只会拿到 Initial 和 Final 两张相片。

WWDC23 10105 - 打造响应更快的相机体验

内存变化

推迟处理,除了体会上的优化,在内存方面也有了显着的优化。 全体试验流程是摄影相片后当即存储到相册内,经过比照是否敞开推迟处理,发现尽管咱们 App 内存没有显着变化,可是全体体系内存显着下降,其间 mediaServerd 进程发生了大幅度的内存变化。

注:以下数据和测验基于 iPhone 14 Pro Max, iOS 17 beta2 版别,数据工具 Xcode Instrument ActivityMonitor。 mediaServer:首要担任音视频的编解码作业,也包含对相片数据的编解码作业。 backBoard:iOS 6 引进的守护进程,用于减轻 SpringBoard 的部分作业量,首要目的是处理来自硬件的事件并将其转发到对应进程。详情

WWDC23 10105 - 打造响应更快的相机体验
WWDC23 10105 - 打造响应更快的相机体验
WWDC23 10105 - 打造响应更快的相机体验
合理猜想: 未敞开推迟,相片处理是在串行队列同步履行的,处理完一张相片,回调给 App,然后持续处理下一张图片,因为阻断了中心流程,因而体系需求分配更多的内存便于快速处理完结。

敞开推迟后,体系知道 App 暂不需求终究的高质量图片,适当下降了内存分配(空闲时内存下降),摄影后,敏捷生成暂时的 Proxy Photo 回调给 App,而终究的相片是放在后台静默履行的,不堵塞用户操作,也无需分配太多内存。其间 backBoardd 的些微涨幅,也能够佐证这一点。 如有任何过错,十分欢迎扔砖交流。

小结

  1. 首要不是一切的相片都合适推迟处理,推迟相片处理是体系主动履行的,假如体系觉得不合适(not suitable),它并不会生成暂时的 Proxy Photo,也不会给咱们回调,而是跟旧版别相同,只会生成终究的相片。因而假如咱们敞开了 isAutoDeferredPhotoProcessingEnabled 后,两个署理办法都要做兼容处理。
  2. 推迟相片处理合适寻求高质量连拍和后期处理的场景,但假如当即获取和分享图画更重要,则不太适用。
  3. 该功用仅支撑 iPhone 11 Pro 及以上。

Zero Shutter Lag 快门零推迟

不知道咱们是否有过疑问,为啥我摄影后的相片总比我摄影的时分要慢很多,尤其是在摄影跳跃的时分,在跳起来的时分我点了摄影,终究生成的相片都是落到地上的相片。也便是相机为什么都做不到零推迟?

当咱们点击相机的摄影按钮时,相机需求必定的时间来完结实际的摄影和处理。具体来说:

  1. 相机首要需求从图画传感器读取数据并生成图画帧,这需求必定时间;
  2. 相机还需求对获取的图画帧使用各种处理,如滤光、白平衡调整、锐化等,这也需求时间;
  3. 假如相机每秒采集 30 帧,那么每帧图画在屏幕上仅逗留 33 毫秒。尽管看起来时间很短,但考虑到摄影目标的移动速度,这极有或许导致摄影时实物方位与终究相片中的方位有必定误差,使相片出现含糊。(想起来当年婚纱照的时分,师傅经常说不要动,坚持住微笑)

所以,当咱们敏捷点击摄影时,动作很快就完毕了,但相机从读取图画到终究生成相片,实际上需求必定时间。这段时间差 frequently 会造成摄影目标在画面中的方位和状况已经发生变化,导致拍出的相片与咱们点击瞬间所见有必定差异,这便是快门推迟导致的问题。

iOS 17 供给了一个 API 能够敞开 “零推迟形式” ,当敞开时,相机的图画管道会坚持之前的帧作为缓冲区。这样在咱们点击摄影时,相机就能够直接从缓冲区中读取最近的一个帧,然后对其进行处理以生成咱们想要的相片。作用比照如下:

WWDC23 10105 - 打造响应更快的相机体验
WWDC23 10105 - 打造响应更快的相机体验

代码上怎么来完结呢?跟推迟处理相同,一个特点来判别是否支撑,一个特点来敞开零推迟。

WWDC23 10105 - 打造响应更快的相机体验
留意:并不是一切的场景都支撑快门零推迟:
WWDC23 10105 - 打造响应更快的相机体验

  1. 闪光灯摄影需求时间充电和同步,难以零推迟。
  2. 手动曝光和对焦需求用户手动设置、相机测光和镜头调整,需求必定时间,会有推迟。
  3. 连拍和多相机同步摄影,需求多个图画传感器和处理器协同作业,时间较长,推迟较大。

留意

  1. 为了完结快门零推迟,iOS 17 新增了 isZeroShutterLagSupported 判别设备是否支撑零推迟,假如支撑即可设置 isZeroShutterLagEnabledtrue 来敞开快门零推迟。可是敞开后假如发现并没有得到想要的作用,最好手动置为 false 关闭该特性。
  2. 敞开零推迟或许会有额外的内存消耗,也或许会比实在调用 capturePhotoWithSettings:delegate: 略微早一点,一起为了削减手抖影响,主张在触发摄影事件和调用 API 之间代码越少越好。

Enabling zero shutter lag reduces or eliminates shutter lag when using AVCapturePhotoQualityPrioritizationBalanced or Quality at the cost of additional memory usage by the photo output. The timestamp of the AVCapturePhoto may be slightly earlier than when -capturePhotoWithSettings:delegate: was called. To minimize camera shake from the user’s tapping gesture it is recommended that -capturePhotoWithSettings:delegate: be called as early as possible when handling the touch down event

New Responsive Capture 新的呼应捕获

WWDC23 10105 - 打造响应更快的相机体验

经过设置 isResponsiveCaptureSupportedisResponsiveCaptureEnabled 来启用呼应捕获,一起翻开上述的isZeroShutterLagEnabled,能够获得重叠的呼应捕获,这样就能够在同一时间内摄影更多相片,进步捕捉完美时间的时机。

相片摄影会从线性的履行调整为并行履行,以供给更快更连贯的接连摄影。可是这会添加内存峰值,一起署理回调顺序也会紊乱,比如在第一张相片didFinishProcessingPhoto 回调之前,或许发生了屡次 willBeginCaptureFor 因而咱们必需求兼容处理相片的回调。

之前咱们只能经过去设置 -> 相机 -> 优先快速摄影,iOS 17 之后咱们能够经过设置 isFastCapturePrioritizationSupportedisFastCapturePrioritizationEnabled 来操控它的开关。敞开之后,假如相机检测到在短时间内接连摄影了多张相片,会相应地将相片质量从最高质量设置调整为更“平衡”的质量设置,以坚持连拍时间距离。

WWDC23 10105 - 打造响应更快的相机体验

摄影状况

前面咱们说到过旧版别相片不处理完结,哪怕重复调用摄影 API,也不会收效,为了确保更好的用户体会,iOS 17 给咱们供给了监听摄影状况的 API,便于咱们办理按钮的状况

WWDC23 10105 - 打造响应更快的相机体验

状况分别为:未运转、就绪、未就绪、等候捕获、等候处理,根据前面的描述咱们了解到,在后面三种状况下,调用 capturePhoto,在摄影和拿到相片之间会需求等候更长期,因而在 not ready 下,强烈主张禁用按钮的交互事件,防止用户长期的等候。

代码完结如下:

if #available(iOS 17.0, *) {
    let readinessCoordination = AVCapturePhotoOutputReadinessCoordinator(photoOutput: photoOutput)
    readinessCoordination.delegate = self
    let photoSettings = AVCapturePhotoSettings()
    // 敞开追寻
    readinessCoordination.startTrackingCaptureRequest(using: photoSettings)
    photoOutput.capturePhoto(with: photoSettings, delegate: self)
}
@available(iOS 17.0, *)
extension CameraViewController: AVCapturePhotoOutputReadinessCoordinatorDelegate {
    func readinessCoordinator(_ coordinator: AVCapturePhotoOutputReadinessCoordinator, captureReadinessDidChange captureReadiness: AVCapturePhotoOutput.CaptureReadiness) {
        // 根据 captureReadiness 值更新摄影按钮的状况
    }
}

Video Effects 视频作用

注:该小点尽管在 session 内,但和本文首要叙述的目标无关,因而简略带过

以前,macOS 的操控中心供给了人像、作业室灯光等相机功用。在 macOS Sonoma 中,视频作用从操控中心移至独自菜单。咱们能够在相机或屏幕共享预览中启用人像、作业室灯光等视频作用,一起支撑调整。

iOS 17 新增 Reactions 作用类型,在视频通话中表达想法或竖起大拇指,Reactions 将视频与气球、彩纸等混合,但不会影响到演讲者。Reactions 遵循人像和作业室灯光作用模板,体系级相机功用,无需使用代码更改。

WWDC23 10105 - 打造响应更快的相机体验

总的来说,新视频作用的 Reactions 特性为视频通话体会带来更丰富的互动,但也提出更高要求,需求开发者妥善处理,更多细节能够看 session 终究一小节和 2021 session What’s new in camera capture

总结

  1. 推迟相片处理能够生成 Proxy 暂时相片,让下一张相片的摄影不再需求等候上一张相片的处理完结,然后加速摄影速度,削减摄影距离。一起对整个 OS 的内存占用也有显着的优化(尽管并非体现在咱们的进程占用中),预计会下降 300M 左右的内存占用。
  2. 快门零推迟经过保持图画帧缓冲区读取历史帧完结,能在咱们点击摄影瞬间近乎零推迟地捕获相片。
  3. 呼应捕获经过并行履行摄影使命,快速接连拍多张相片,以获取更多完美时间。
  4. 能够经过监听摄影状况来办理摄影按钮,防止用户长期等候。
  5. 总归,经过推迟相片处理、快门零推迟、呼应捕获等新特性,以及状况监听等办法,咱们能够大幅进步相机呼应速度,发明更流通的摄影体会。

参考资料

  1. Create a more responsive camera experience
  2. What’s new in camera capture
  3. Handle the Limited Photos Library in your app