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

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"))

总结

  1. 对于DJI相机问题:必须等设备连接成功后再获取相机,按照DJI SDK的流程走就不会返回nil;
  2. 对于原生相机黑屏问题:用硬件加速的转换方法,避免手动逐像素操作,同时处理好线程和内存,保证像素格式匹配。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.29 08:25:54