You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

macOS应用中AVCapturePhotoOutput无法捕获保存图片的问题

macOS应用摄像头捕获图片失败问题排查

问题描述

开发macOS应用,点击按钮调用前置摄像头捕获图片,摄像头绿灯亮起但图片未保存,控制台输出以下错误日志:

2023-08-13 13:02:54.978411+0200 button_to_image[23599:3209684] Metal API Validation Enabled
2023-08-13 13:02:55.155150+0200 button_to_image[23599:3209684] [] CMIO_DAL_PlugInManagement.cpp:880:CreatePlugIn Could not find plugin with kCMIOHardwarePlugInTypeID
2023-08-13 13:02:55.175844+0200 button_to_image[23599:3209684] [plugin] AddInstanceForFactory: No factory registered for id <CFUUID 0x6000039a00a0> F8BB1C28-BAE8-11D6-9C31-00039315CD46
objc[23599]: class `NSKVONotifying_AVCapturePhotoOutput' not linked into application

已完成的配置:

  • App Sandbox和Hardened Runtime中勾选摄像头权限
  • 添加AVFoundation.framework框架
  • Info.plist中添加NSCameraUsageDescription字段
  • 尝试过模拟器运行、导出本地运行,均未解决

相关代码:

import SwiftUI
import AVFoundation
import AppKit
import Foundation

struct ContentView: View {
    @State private var capturedImage: NSImage? = nil

    var body: some View {
        VStack {
            Image(systemName: "globe")
                .imageScale(.large)
                .foregroundColor(.accentColor)
            Text("Click the button!")
            Button(action: {
                captureImage()
            }) {
                Text("Capture Image")
            }
            if let img = capturedImage {
                Image(nsImage: img)
                    .resizable()
                    .aspectRatio(contentMode: .fit)
            }
        }
        .padding()
    }
    
    func captureImage() {
        let captureSession = AVCaptureSession()
        guard let captureDevice = AVCaptureDevice.default(for: .video) else { return }
        guard let input = try? AVCaptureDeviceInput(device: captureDevice) else { return }
        captureSession.addInput(input)
        captureSession.startRunning()
        
        let output = AVCapturePhotoOutput()
        captureSession.addOutput(output)
        
        let settings = AVCapturePhotoSettings()
        output.capturePhoto(with: settings, delegate: PhotoCaptureDelegate { image in
            self.capturedImage = image
        })
    }
}

class PhotoCaptureDelegate: NSObject, AVCapturePhotoCaptureDelegate {
    let completionHandler: (NSImage?) -> Void

    init(completionHandler: @escaping (NSImage?) -> Void) {
        self.completionHandler = completionHandler
    }

    func photoOutput(_ output: AVCapturePhotoOutput, didFinishProcessingPhoto photo: AVCapturePhoto, error: Error?) {
        guard let imageData = photo.fileDataRepresentation() else { return }
        let image = NSImage(data: imageData)
        completionHandler(image)

        // Save the image to a file
        let desktopURL = FileManager.default.urls(for: .desktopDirectory, in: .userDomainMask).first!
        let fileURL = desktopURL.appendingPathComponent("capturedImage.jpg")
        try? imageData.write(to: fileURL)
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

问题分析与解决方案

核心问题1:AVCaptureSession生命周期过短

captureImage()方法中AVCaptureSession是局部变量,方法执行完毕后会被立即释放,导致捕获流程中断,无法触发didFinishProcessingPhoto回调。

核心问题2:AVCapturePhotoOutput添加时机错误

必须在captureSession.startRunning()之前添加输出,否则会话运行后添加输出可能不生效。

核心问题3:try?掩盖错误

保存图片时使用try?会忽略写入错误,无法排查权限或路径问题。

修复后的代码

import SwiftUI
import AVFoundation
import AppKit
import Foundation

struct ContentView: View {
    @State private var capturedImage: NSImage? = nil
    // 将captureSession提升为实例变量,延长生命周期
    private var captureSession: AVCaptureSession?

    var body: some View {
        VStack {
            Image(systemName: "globe")
                .imageScale(.large)
                .foregroundColor(.accentColor)
            Text("Click the button!")
            Button(action: {
                captureImage()
            }) {
                Text("Capture Image")
            }
            if let img = capturedImage {
                Image(nsImage: img)
                    .resizable()
                    .aspectRatio(contentMode: .fit)
            }
        }
        .padding()
    }
    
    func captureImage() {
        let session = AVCaptureSession()
        captureSession = session // 保存引用
        
        // 配置会话为照片预设
        session.sessionPreset = .photo
        
        // 明确指定前置摄像头
        guard let captureDevice = AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: .front) else {
            print("无法获取前置摄像头")
            return
        }
        
        do {
            let input = try AVCaptureDeviceInput(device: captureDevice)
            if session.canAddInput(input) {
                session.addInput(input)
            }
            
            let output = AVCapturePhotoOutput()
            if session.canAddOutput(output) {
                session.addOutput(output) // 先添加输出再启动会话
            }
            
            // 在后台队列启动会话,避免阻塞主线程
            DispatchQueue.global(qos: .userInitiated).async {
                session.startRunning()
                
                // 延迟捕获,确保会话完全启动
                DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
                    let settings = AVCapturePhotoSettings()
                    output.capturePhoto(with: settings, delegate: PhotoCaptureDelegate { [weak self] image in
                        guard let self = self else { return }
                        self.capturedImage = image
                        // 捕获完成后停止会话并释放引用
                        self.captureSession?.stopRunning()
                        self.captureSession = nil
                    })
                }
            }
        } catch {
            print("配置摄像头失败: \(error.localizedDescription)")
        }
    }
}

class PhotoCaptureDelegate: NSObject, AVCapturePhotoCaptureDelegate {
    let completionHandler: (NSImage?) -> Void

    init(completionHandler: @escaping (NSImage?) -> Void) {
        self.completionHandler = completionHandler
    }

    func photoOutput(_ output: AVCapturePhotoOutput, didFinishProcessingPhoto photo: AVCapturePhoto, error: Error?) {
        if let error = error {
            print("捕获图片失败: \(error.localizedDescription)")
            completionHandler(nil)
            return
        }
        
        guard let imageData = photo.fileDataRepresentation() else {
            print("无法获取图片数据")
            completionHandler(nil)
            return
        }
        
        let image = NSImage(data: imageData)
        completionHandler(image)

        // 保存图片并处理错误
        let desktopURL = FileManager.default.urls(for: .desktopDirectory, in: .userDomainMask).first!
        let fileURL = desktopURL.appendingPathComponent("capturedImage.jpg")
        do {
            try imageData.write(to: fileURL)
            print("图片已保存到: \(fileURL.path)")
        } catch {
            print("保存图片失败: \(error.localizedDescription)")
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

额外配置检查

  • App Sandbox权限:除摄像头权限外,需勾选文件访问权限中的「User Selected File」,同时在Info.plist中添加NSDesktopFolderUsageDescription字段,说明访问桌面的原因。
  • Hardened Runtime:确保「Camera」权限已勾选,未禁用必要系统功能。
  • 测试环境:macOS模拟器不支持摄像头硬件,必须在真实Mac设备上测试。

内容的提问来源于stack exchange,提问作者yemy

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.13 11:49:52