DJI Osmo Mobile2开发:DJIHandheld相机为nil及原生预览黑屏求助
解决DJI Osmo Mobile2相机获取失败与原生AVCapture预览黑屏问题
嘿,我来帮你梳理下这两个问题——DJI相机获取失败和原生相机预览黑屏,咱们一个个来解决:
一、DJIHandheld相机返回nil的问题
这个问题大概率是设备连接时机不对导致的。你得确保DJISDK已经完成激活,并且DJI设备(Osmo Mobile2)已经成功连接后,再去获取相机对象,不然肯定会返回nil。
修复步骤:
先完成DJISDK的注册和设备连接监听:
import DJISDK class YourViewController: UIViewController, DJISDKManagerDelegate { override func viewDidLoad() { super.viewDidLoad() // 注册SDK DJISDKManager.registerApp(with: self) { [weak self] status, error in guard status == .registered else { print("SDK注册失败: \(error?.localizedDescription ?? "未知错误")") return } // 开始连接设备 DJISDKManager.startConnectionToProduct() } // 设置SDK代理 DJISDKManager.delegate = self } // 监听产品连接状态 func sdkManager(_ manager: DJISDKManager, didConnectTo product: DJIBaseProduct) { // 确认连接的是Osmo Mobile2(DJIHandheld类型) guard let handheld = product as? DJIHandheld else { print("连接的不是手持设备") return } // 这里再获取相机,就不会返回nil了 let camera = handheld.camera print("成功获取DJI相机: \(camera)") // 接下来可以设置相机参数、开启预览等 } func sdkManager(_ manager: DJISDKManager, didDisconnectFrom product: DJIBaseProduct) { print("设备断开连接") } }
一定要等didConnectTo回调触发后,再去获取相机,这是DJI SDK的标准流程。
二、原生AVCapture预览黑屏的问题
你的代码里有几个关键问题导致黑屏,咱们逐一排查:
1. 手动逐像素转换效率低且易出错
你用双重循环手动把YUV转RGBA,不仅效率极低(会阻塞相机帧捕获线程),而且YUV格式的假设可能不对:iOS相机默认输出的是NV12格式(Y平面 + UV交错平面),但你的代码按YUV420P(Y平面 + U平面 + V平面)来计算索引,转换出来的像素数据完全错误,自然会黑屏。
2. 内存泄漏与线程阻塞
- 你用
UnsafeMutablePointer<UInt8>.allocate分配了内存,但没有释放,会导致内存泄漏; captureOutput回调是在AVCapture的串行队列执行的,你在里面做大量计算会阻塞队列,导致相机无法输出新的帧,进而黑屏。
3. 像素格式不匹配
VideoPreviewer基于ffmpeg,它可能更适配原生YUV格式(比如NV12),你转成RGBA反而可能不兼容,导致无法渲染。
修复方案:用硬件加速转换+正确的内存/线程管理
推荐用Accelerate框架的vImage做硬件加速的YUV转RGBA,既高效又准确,同时处理好线程和内存:
第一步:添加Accelerate框架依赖
在你的项目中导入Accelerate框架,然后写一个硬件加速的转换函数:
import Accelerate private func convertNV12ToRGBA(pixelBuffer: CVPixelBuffer) -> Data? { CVPixelBufferLockBaseAddress(pixelBuffer, .readOnly) defer { CVPixelBufferUnlockBaseAddress(pixelBuffer, .readOnly) } // 获取Y和UV平面的地址 guard let yBaseAddr = CVPixelBufferGetBaseAddressOfPlane(pixelBuffer, 0), let uvBaseAddr = CVPixelBufferGetBaseAddressOfPlane(pixelBuffer, 1) else { return nil } let width = CVPixelBufferGetWidth(pixelBuffer) let height = CVPixelBufferGetHeight(pixelBuffer) let yRowBytes = CVPixelBufferGetBytesPerRowOfPlane(pixelBuffer, 0) let uvRowBytes = CVPixelBufferGetBytesPerRowOfPlane(pixelBuffer, 1) // 配置RGBA输出格式 var rgbaFormat = vImage_CGImageFormat( bitsPerComponent: 8, bitsPerPixel: 32, colorSpace: CGColorSpaceCreateDeviceRGB(), bitmapInfo: CGBitmapInfo(rawValue: CGImageAlphaInfo.premultipliedLast.rawValue), renderingIntent: .defaultIntent ) // 初始化Y、UV、RGBA的vImage缓冲区 var yBuffer = vImage_Buffer(data: yBaseAddr, height: vImagePixelCount(height), width: vImagePixelCount(width), rowBytes: yRowBytes) var uvBuffer = vImage_Buffer(data: uvBaseAddr, height: vImagePixelCount(height/2), width: vImagePixelCount(width/2), rowBytes: uvRowBytes) var rgbaBuffer = vImage_Buffer() guard vImageBuffer_Init(&rgbaBuffer, vImagePixelCount(height), vImagePixelCount(width), 32, vImage_Flags(kvImageNoFlags)) == kvImageNoError else { return nil } // 执行硬件加速转换(NV12转RGBA) let convertStatus = vImageConvert_420YpCbCr8ToARGB8888( &yBuffer, &uvBuffer, &rgbaBuffer, nil, 0, vImage_Flags(kvImageNoFlags) ) guard convertStatus == kvImageNoError else { free(rgbaBuffer.data) return nil } // 将RGBA缓冲区转为Data,自动管理内存释放 return Data(bytesNoCopy: rgbaBuffer.data, count: rgbaBuffer.rowBytes * height, deallocator: .free) }
第二步:修改captureOutput回调
把转换逻辑放到后台线程,避免阻塞相机队列,同时正确管理内存:
func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) { guard let pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) else { return } // 放到后台线程处理转换,不阻塞相机帧捕获 DispatchQueue.global(qos: .userInitiated).async { [weak self] in guard let rgbaData = self?.convertNV12ToRGBA(pixelBuffer: pixelBuffer) else { return } // 分配内存并复制数据 let videoBuffer = UnsafeMutablePointer<UInt8>.allocate(capacity: rgbaData.count) defer { videoBuffer.deallocate() } // 确保内存被释放 rgbaData.copyBytes(to: videoBuffer, count: rgbaData.count) // 推送到VideoPreviewer VideoPreviewer.instance().push(videoBuffer, length: Int32(rgbaData.count)) } }
第三步:设置相机输出格式为NV12
在初始化AVCaptureVideoDataOutput时,指定输出格式为NV12,这是iOS相机最常用的格式:
let videoOutput = AVCaptureVideoDataOutput() // 设置输出格式为NV12 videoOutput.videoSettings = [ kCVPixelBufferPixelFormatTypeKey as String: kCVPixelFormatType_420YpCbCr8BiPlanarFullRange ] // 设置回调队列(建议用串行队列) videoOutput.setSampleBufferDelegate(self, queue: DispatchQueue(label: "com.yourcompany.cameraQueue"))
总结
- 对于DJI相机问题:必须等设备连接成功后再获取相机,按照DJI SDK的流程走就不会返回nil;
- 对于原生相机黑屏问题:用硬件加速的转换方法,避免手动逐像素操作,同时处理好线程和内存,保证像素格式匹配。
内容的提问来源于stack exchange,提问作者JaSHin
相关产品推荐
相关产品推荐

