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

AVCaptureVideoOutput结合MultipeerConnectivity流摄像头预览问题排查与优化

问题

正在开发一款摄像头遥控应用,iPhone作为摄像头端、iPad作为遥控端。通过AVCaptureVideoOutput获取iPhone摄像头预览画面,借助MultipeerConnectivity框架的OutputStream传输流数据,iPad接收后设置UIView.layer.contents显示画面。

当前实现代码如下:

iPhone(摄像头端)AVCaptureVideoDataOutputSampleBufferDelegate实现

func captureOutput(_ output: AVCaptureOutput,
                   didOutput sampleBuffer: CMSampleBuffer,
                   from connection: AVCaptureConnection) {
    DispatchQueue.global(qos: .utility).async { [unowned self] in
        let imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer)
        
        if let imageBuffer {
            CVPixelBufferLockBaseAddress(imageBuffer, [])
            let baseAddress = CVPixelBufferGetBaseAddress(imageBuffer)
            let bytesPerRow: size_t? = CVPixelBufferGetBytesPerRow(imageBuffer)
            let width: size_t? = CVPixelBufferGetWidth(imageBuffer)
            let height: size_t? = CVPixelBufferGetHeight(imageBuffer)
            
            let colorSpace = CGColorSpaceCreateDeviceRGB()
            let newContext = CGContext(data: baseAddress,
                                       width: width ?? 0,
                                       height: height ?? 0,
                                       bitsPerComponent: 8,
                                       bytesPerRow: bytesPerRow ?? 0,
                                       space: colorSpace,
                                       bitmapInfo: CGBitmapInfo.byteOrder32Little.rawValue | CGImageAlphaInfo.premultipliedFirst.rawValue)
            
            if let newImage = newContext?.makeImage() {
                let image = UIImage(cgImage: newImage,
                                    scale: 0.2,
                                    orientation: .up)
                
                CVPixelBufferUnlockBaseAddress(imageBuffer, [])
                
                if let data = image.jpegData(compressionQuality: 0.2) {
                    let bytesWritten = data.withUnsafeBytes({
                        viewFinderStream?
                            .write($0.bindMemory(to: UInt8.self).baseAddress!, maxLength: data.count)
                    })
                }
            }
        }
    }
}

iPad(遥控端)StreamDelegate实现

func stream(_ aStream: Stream, handle eventCode: Stream.Event) {
    let inputStream = aStream as! InputStream

    switch eventCode {
    case .hasBytesAvailable:
        DispatchQueue.global(qos: .userInteractive).async { [unowned self] in
            var buffer = [UInt8](repeating: 0, count: 1024)
            let numberBytes = inputStream.read(&buffer, maxLength: 1024)
            let data = Data(referencing: NSData(bytes: &buffer, length: numberBytes))
            if let imageData = UIImage(data: data) {
                DispatchQueue.main.async {
                    previewCameraView.layer.contents = imageData.cgImage
                }
            }
        }
    case .hasSpaceAvailable:
        break
    default:
        break
    }
}

遇到的问题:

  • iPad仅显示极小部分画面(仅视图左上角少量像素,其余灰色),控制台出现警告:

2023-02-02 20:24:44.487399+0700 MultipeerVideo-Assignment[31170:1065836] Warning! [0x15c023800] Decoding incomplete with error code -1. This is expected if the image has not been fully downloaded.

  • iPhone CPU占用率接近100%
解决方案

一、画面显示不全的原因及修复

核心问题

发送端直接将整张图片的JPEG数据写入输出流,但接收端每次仅读取1024字节就尝试解码。一张压缩后的JPEG图片远大于1024字节,导致每次读取的都是不完整的JPEG数据,无法解码出完整图片,进而出现仅显示部分像素的情况。

修复步骤

  1. 发送端:添加数据长度标识
    在发送图片数据前,先将数据长度转换为固定4字节的UInt32(大端字节序)写入流,让接收端先知晓完整图片的字节数:

    if let data = image.jpegData(compressionQuality: 0.2) {
        // 先写入数据长度
        let length = UInt32(data.count).bigEndian
        let lengthData = Data(bytes: &length, count: MemoryLayout<UInt32>.size)
        _ = lengthData.withUnsafeBytes {
            viewFinderStream?.write($0.bindMemory(to: UInt8.self).baseAddress!, maxLength: lengthData.count)
        }
        // 再写入图片数据
        let bytesWritten = data.withUnsafeBytes({
            viewFinderStream?
                .write($0.bindMemory(to: UInt8.self).baseAddress!, maxLength: data.count)
        })
    }
    
  2. 接收端:缓存数据直到完整接收
    维护一个缓冲区,先读取长度标识,再根据长度接收足够字节后再解码:

    // 类级变量存储缓冲区和待接收长度
    private var receiveBuffer = Data()
    private var expectedLength: UInt32?
    
    func stream(_ aStream: Stream, handle eventCode: Stream.Event) {
        let inputStream = aStream as! InputStream
    
        switch eventCode {
        case .hasBytesAvailable:
            DispatchQueue.global(qos: .userInteractive).async { [unowned self] in
                var buffer = [UInt8](repeating: 0, count: 1024)
                while inputStream.hasBytesAvailable {
                    let bytesRead = inputStream.read(&buffer, maxLength: buffer.count)
                    guard bytesRead > 0 else { break }
                    receiveBuffer.append(buffer.prefix(bytesRead))
                    
                    // 解析长度标识
                    if expectedLength == nil {
                        if receiveBuffer.count >= MemoryLayout<UInt32>.size {
                            let lengthData = receiveBuffer.prefix(MemoryLayout<UInt32>.size)
                            expectedLength = lengthData.withUnsafeBytes {
                                $0.load(as: UInt32.self).bigEndian
                            }
                            receiveBuffer.removeFirst(MemoryLayout<UInt32>.size)
                        }
                    }
                    
                    // 检查是否接收完整数据
                    if let expectedLength = expectedLength, receiveBuffer.count >= Int(expectedLength) {
                        let imageData = receiveBuffer.prefix(Int(expectedLength))
                        if let image = UIImage(data: imageData) {
                            DispatchQueue.main.async {
                                self.previewCameraView.layer.contents = image.cgImage
                            }
                        }
                        // 重置缓冲区和长度标识
                        receiveBuffer.removeFirst(Int(expectedLength))
                        self.expectedLength = nil
                    }
                }
            }
        case .hasSpaceAvailable:
            break
        default:
            break
        }
    }
    

二、降低iPhone CPU占用率的优化方法

1. 跳过不必要的像素格式转换

直接对CVPixelBuffer进行JPEG编码,避免转换为CGImage和UIImage的开销:

if let imageBuffer {
    CVPixelBufferLockBaseAddress(imageBuffer, [])
    // 直接编码CVPixelBuffer为JPEG
    let ciImage = CIImage(cvPixelBuffer: imageBuffer)
    let context = CIContext(options: [.useSoftwareRenderer: false]) // 启用硬件加速
    if let jpegData = context.jpegRepresentation(of: ciImage, colorSpace: CGColorSpaceCreateDeviceRGB(), options: [.quality: 0.2]) {
        // 写入长度和数据(同之前的发送逻辑)
        let length = UInt32(jpegData.count).bigEndian
        let lengthData = Data(bytes: &length, count: MemoryLayout<UInt32>.size)
        _ = lengthData.withUnsafeBytes {
            viewFinderStream?.write($0.bindMemory(to: UInt8.self).baseAddress!, maxLength: lengthData.count)
        }
        _ = jpegData.withUnsafeBytes {
            viewFinderStream?.write($0.bindMemory(to: UInt8.self).baseAddress!, maxLength: jpegData.count)
        }
    }
    CVPixelBufferUnlockBaseAddress(imageBuffer, [])
}

2. 降低摄像头采集分辨率

在配置AVCaptureSession时选择更低的预设分辨率,减少数据处理量:

let captureSession = AVCaptureSession()
captureSession.sessionPreset = .vga640x480 // 或更低的.qvga320x240

3. 限制采集帧率

通过AVCaptureVideoDataOutput限制帧率,减少每秒处理的画面数量:

let videoOutput = AVCaptureVideoDataOutput()
videoOutput.minFrameDuration = CMTimeMake(value: 1, timescale: 15) // 限制为15帧/秒

4. 确保后台线程处理所有耗时操作

保持编码、流写入等操作在后台队列执行,避免阻塞主线程,同时避免不必要的线程切换。


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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.02 20:41:07