iosuikitavfoundationvisionos

How to pass UIViewRepresentable with a AVSampleBufferDisplayLayer to SwiftUI


When the view loads on device I just have a black screen. I have very similar code working with a Storyboard instance and using UIViewController, but I need to get away from that as I am working with visionOS and would like to abstract this away into using RealityKit and VideoPlayerLayer, but for prototyping and iterating other tasks I would like this AVSampleBufferDisplayLayer to work... which ultimately I will have to pass an AVSampleBufferVideoRenderer to VideoPlayerLayer so getting this implementation works helps me dive into the RealityKit rendering.

Here is the code, I've stuffed everything into one file for ease of debugging and questions.

Thank you!

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
    }
    
    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()
}

I created a UIKit version of this and successfully loaded as an iOS device in the AVP's.

Here is the ViewController running that code:


class ViewController: UIViewController { //, VideoDecoderAnnexBAdaptorDelegate {
    
    // 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
    }
}


Solution

  • In your init LayerView code:

    func makeUIView(context: Context) -> UIView {
        print("LayerUIView is being created")
        return LayerUIView()         
    }
    

    Initial frame for LayerUIView is .zero. Therefore your displayLayer's frame is initially .zero so it is not show properly on screen, moreover after LayerUIView is init, displayeLayer's frame is not updated anymore.

    So in order to fix your problem, you need to update frame for displayLayer when layoutSubviews too. In LayerUIView, instead of update frame for layer:

    override func layoutSubviews() {
        super.layoutSubviews()
        layer.frame = bounds
    }
    

    Use this code instead:

    override func layoutSubviews() {
        super.layoutSubviews()
        displayLayer.frame = bounds
    }