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

SwiftUI集成带AVSampleBufferDisplayLayer的UIViewRepresentable黑屏问题排查

解决visionOS中SwiftUI下AVSampleBufferDisplayLayer黑屏问题

问题描述

在visionOS开发中,尝试脱离UIViewController架构,改用SwiftUI结合RealityKit/VideoPlayerLayer方案,当前原型阶段需要让AVSampleBufferDisplayLayer正常工作,但加载视图时仅显示黑屏。相同逻辑的UIKit(Storyboard+UIViewController)版本可在iOS/AVP设备上正常运行。

现有代码

SwiftUI版本(存在黑屏问题)

struct MirrorView: View {
    var body: some View {
        VStack {
            LayerView()
        }
    }
}

struct LayerView: UIViewRepresentable {
    func makeUIView(context: Context) -> UIView {
        print("LayerUIView is being created")
        return LayerUIView()
            
    }
    
    func updateUIView(_ uiView: UIView, context: UIViewRepresentableContext<LayerView>) {
        print("LayerUIView is being updated")
    }
}

class LayerUIView: UIView {
    
    private let networking = Networking()
    private let displayLayer = AVSampleBufferDisplayLayer()
    private var subscriptions = Set<AnyCancellable>()
    private var sampleBufferTask: Task<Void, Never>?
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        print("LayerUIView initialized")
        setupVideoLayer()
        setupNetworking()
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    override func layoutSubviews() {
        super.layoutSubviews()
        layer.frame = bounds // 错误点:未更新视频层frame
    }
    
    private func setupVideoLayer() {
        
        displayLayer.frame = bounds
        displayLayer.videoGravity = .resizeAspect
        
        layer.addSublayer(displayLayer)
        NotificationCenter.default.addObserver(
                self,
                selector: #selector(handleFailedToDecodeNotification(_:)),
                name: .AVSampleBufferDisplayLayerFailedToDecode,
                object: displayLayer
            )
        }

        @objc private func handleFailedToDecodeNotification(_ notification: Notification) {
            if let error = notification.userInfo?[AVSampleBufferDisplayLayerFailedToDecodeNotificationErrorKey] {
                print("Failed to decode sample buffer. Error: \(error)")
            } else {
                print("Failed to decode sample buffer. No error information available.")
            }
        }
    
    private func setupNetworking() {
        networking.startAdvertising()
        print("Networking is connected: \(networking.isConnected)")
        startSampleBufferTask()
    }

    // MARK: - Task Management
    private func startSampleBufferTask() {
        
        sampleBufferTask = Task {
            for await sampleBuffer in networking.sampleBuffers {
                let formatDescription = CMSampleBufferGetFormatDescription(sampleBuffer)
                print("Format Description: \(String(describing: formatDescription))")

                let presentationTimeStamp = CMSampleBufferGetPresentationTimeStamp(sampleBuffer)
                print("Presentation Timestamp: \(presentationTimeStamp)")

                let duration = CMSampleBufferGetDuration(sampleBuffer)
                print("Duration: \(duration)")
                
                DispatchQueue.main.async {
                    self.displayLayer.sampleBufferRenderer.enqueue(sampleBuffer)
                }
            }
        }
    }

    private func stopSampleBufferTask() {
        sampleBufferTask?.cancel()
        sampleBufferTask = nil
    }
}

#Preview {
    MirrorView()
}

UIKit版本(正常运行)

class ViewController: UIViewController {
    
    // MARK: - Properties
    private let networking = Networking()
    private let displayLayer = AVSampleBufferDisplayLayer()
    private var subscriptions = Set<AnyCancellable>()
    private var sampleBufferTask: Task<Void, Never>?
    
    // MARK: - Lifecycle
    override func viewDidLoad() {
        super.viewDidLoad()
        
        setupVideoLayer()
        setupNetworking()
    }
    
    override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
        return .allButUpsideDown
    }

    override var shouldAutorotate: Bool {
        return true
    }
    
    override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
        super.viewWillTransition(to: size, with: coordinator)
        
        coordinator.animate(alongsideTransition: { _ in
            // Adjust layout for new orientation
            self.displayLayer.frame = self.view.bounds
        }, completion: nil)
    }
    
    // MARK: - Setup Methods
    private func setupVideoLayer() {
        displayLayer.frame = view.bounds
        displayLayer.videoGravity = .resizeAspect
        displayLayer.backgroundColor = UIColor.black.cgColor
        view.layer.addSublayer(displayLayer)
        NotificationCenter.default.addObserver(
                self,
                selector: #selector(handleFailedToDecodeNotification(_:)),
                name: .AVSampleBufferDisplayLayerFailedToDecode,
                object: displayLayer
            )
        }

        @objc private func handleFailedToDecodeNotification(_ notification: Notification) {
            if let error = notification.userInfo?[AVSampleBufferDisplayLayerFailedToDecodeNotificationErrorKey] {
                print("Failed to decode sample buffer. Error: \(error)")
            } else {
                print("Failed to decode sample buffer. No error information available.")
            }
        }
    
    private func setupNetworking() {
        networking.startAdvertising()
        print("Networking is connected: \(networking.isConnected)")
        startSampleBufferTask()
    }

    // MARK: - Task Management
    private func startSampleBufferTask() {
        
        sampleBufferTask = Task {
            for await sampleBuffer in networking.sampleBuffers {
                let formatDescription = CMSampleBufferGetFormatDescription(sampleBuffer)
                print("Format Description: \(String(describing: formatDescription))")

                let presentationTimeStamp = CMSampleBufferGetPresentationTimeStamp(sampleBuffer)
                print("Presentation Timestamp: \(presentationTimeStamp)")

                let duration = CMSampleBufferGetDuration(sampleBuffer)
                print("Duration: \(duration)")
                
                DispatchQueue.main.async {
                    self.displayLayer.sampleBufferRenderer.enqueue(sampleBuffer)
                }
            }
        }
    }

    private func stopSampleBufferTask() {
        sampleBufferTask?.cancel()
        sampleBufferTask = nil
    }
}

问题排查与修复

对比两份代码,SwiftUI版本的核心问题在于视频层的布局同步错误:

  1. 修正layoutSubviews方法:
    原代码中layoutSubviews更新的是自身layer.frame = bounds,但需要同步更新displayLayer的frame,确保视频层始终匹配父视图的尺寸。初始化时bounds可能为CGRect.zero,后续视图布局时必须更新视频层的frame才能正常显示。

  2. 添加displayLayer背景色(可选):
    和UIKit版本保持一致,设置displayLayer.backgroundColor = UIColor.black.cgColor,避免视频未加载时显示透明背景。

修正后的LayerUIView关键代码

class LayerUIView: UIView {
    
    // ... 其他属性保持不变 ...
    
    override func layoutSubviews() {
        super.layoutSubviews()
        displayLayer.frame = bounds // 替换原有的layer.frame = bounds
    }
    
    private func setupVideoLayer() {
        
        displayLayer.frame = bounds
        displayLayer.videoGravity = .resizeAspect
        displayLayer.backgroundColor = UIColor.black.cgColor // 添加背景色
        
        layer.addSublayer(displayLayer)
        NotificationCenter.default.addObserver(
                self,
                selector: #selector(handleFailedToDecodeNotification(_:)),
                name: .AVSampleBufferDisplayLayerFailedToDecode,
                object: displayLayer
            )
        }
    
    // ... 其他方法保持不变 ...
}

额外检查点

如果修复后仍黑屏,可检查以下内容:

  • 确认networking.sampleBuffers输出的CMSampleBuffer格式正确,包含有效的格式描述和时间戳
  • 检查AVSampleBufferDisplayLayerFailedToDecode通知是否触发,排查解码错误
  • 确保sampleBufferRenderer.enqueue调用在主线程(当前代码已满足)

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.15 07:49:52