Repository navigation
Add composite view capture support for Apple Vision Pro #20
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,243 @@ | ||||||||||||||||||||||||||
| import ReplayKit | ||||||||||||||||||||||||||
| import AVFoundation | ||||||||||||||||||||||||||
| import MetalKit | ||||||||||||||||||||||||||
| import Accelerate | ||||||||||||||||||||||||||
| import Foundation | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| // Composite View Capture using ReplayKit | ||||||||||||||||||||||||||
| // This captures both the physical passthrough and digital content in real-time | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| var compositeCurrentTexture: MTLTexture? | ||||||||||||||||||||||||||
| let compositeMtlDevice: MTLDevice = MTLCreateSystemDefaultDevice()! | ||||||||||||||||||||||||||
| var compositeTextureCache: CVMetalTextureCache! = nil | ||||||||||||||||||||||||||
| var compositeCommandQueue: MTLCommandQueue! | ||||||||||||||||||||||||||
| var compositePointer: UnsafeMutableRawPointer! = nil | ||||||||||||||||||||||||||
| var isCompositeRunning: Bool = false | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| @_cdecl("startCompositeCapture") | ||||||||||||||||||||||||||
| public func startCompositeCapture() { | ||||||||||||||||||||||||||
| print("############ START COMPOSITE CAPTURE ############") | ||||||||||||||||||||||||||
| isCompositeRunning = true | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| let screenRecorder = RPScreenRecorder.shared() | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| // Disable microphone to avoid audio capture (as per requirements) | ||||||||||||||||||||||||||
| screenRecorder.isMicrophoneEnabled = false | ||||||||||||||||||||||||||
| screenRecorder.isCameraEnabled = false | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| // Start capture with handler for each frame | ||||||||||||||||||||||||||
| screenRecorder.startCapture(handler: { (sampleBuffer, bufferType, error) in | ||||||||||||||||||||||||||
| if let error = error { | ||||||||||||||||||||||||||
| print("Composite capture error: \(error.localizedDescription)") | ||||||||||||||||||||||||||
| return | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| // Only process video frames | ||||||||||||||||||||||||||
| guard bufferType == .video else { return } | ||||||||||||||||||||||||||
| guard isCompositeRunning else { return } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| // Get the pixel buffer from the sample buffer | ||||||||||||||||||||||||||
| guard let pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) else { | ||||||||||||||||||||||||||
| print("Failed to get pixel buffer from sample buffer") | ||||||||||||||||||||||||||
| return | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| createCompositeTexture(pixelBuffer) | ||||||||||||||||||||||||||
| }, completionHandler: { error in | ||||||||||||||||||||||||||
| if let error = error { | ||||||||||||||||||||||||||
| print("Failed to start composite capture: \(error.localizedDescription)") | ||||||||||||||||||||||||||
| isCompositeRunning = false | ||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||
| print("Composite capture started successfully") | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| @_cdecl("stopCompositeCapture") | ||||||||||||||||||||||||||
| public func stopCompositeCapture() { | ||||||||||||||||||||||||||
| print("############ STOP COMPOSITE CAPTURE ##############") | ||||||||||||||||||||||||||
| isCompositeRunning = false | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| let screenRecorder = RPScreenRecorder.shared() | ||||||||||||||||||||||||||
| screenRecorder.stopCapture { error in | ||||||||||||||||||||||||||
| if let error = error { | ||||||||||||||||||||||||||
| print("Failed to stop composite capture: \(error.localizedDescription)") | ||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||
| print("Composite capture stopped successfully") | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
Comment on lines
+56
to
+69
|
||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| @_cdecl("getCompositeTexture") | ||||||||||||||||||||||||||
| public func getCompositeTexture() -> UnsafeMutableRawPointer? { | ||||||||||||||||||||||||||
| return compositePointer | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| @_cdecl("isCompositeAvailable") | ||||||||||||||||||||||||||
| public func isCompositeAvailable() -> Bool { | ||||||||||||||||||||||||||
| return RPScreenRecorder.shared().isAvailable | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| private func createCompositeTexture(_ pixelBuffer: CVPixelBuffer) { | ||||||||||||||||||||||||||
|
Comment on lines
+10
to
+81
|
||||||||||||||||||||||||||
| guard let pixelBufferBGRA: CVPixelBuffer = try? pixelBuffer.toCompositeBGRA() else { return } | ||||||||||||||||||||||||||
| let width = CVPixelBufferGetWidth(pixelBufferBGRA) | ||||||||||||||||||||||||||
| let height = CVPixelBufferGetHeight(pixelBufferBGRA) | ||||||||||||||||||||||||||
| var cvTexture: CVMetalTexture? | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| if compositeTextureCache == nil { | ||||||||||||||||||||||||||
| CVMetalTextureCacheCreate(kCFAllocatorDefault, nil, compositeMtlDevice, nil, &compositeTextureCache) | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| let textureStatus = CVMetalTextureCacheCreateTextureFromImage(kCFAllocatorDefault, | ||||||||||||||||||||||||||
| compositeTextureCache, | ||||||||||||||||||||||||||
| pixelBufferBGRA, | ||||||||||||||||||||||||||
| nil, | ||||||||||||||||||||||||||
| .bgra8Unorm_srgb, | ||||||||||||||||||||||||||
| width, | ||||||||||||||||||||||||||
| height, | ||||||||||||||||||||||||||
| 0, | ||||||||||||||||||||||||||
| &cvTexture) | ||||||||||||||||||||||||||
| guard textureStatus == kCVReturnSuccess, | ||||||||||||||||||||||||||
| let imageTexture = cvTexture, | ||||||||||||||||||||||||||
| let texture = CVMetalTextureGetTexture(imageTexture) else { return } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| if compositeCurrentTexture == nil { | ||||||||||||||||||||||||||
| let texdescriptor = MTLTextureDescriptor.texture2DDescriptor(pixelFormat: texture.pixelFormat, | ||||||||||||||||||||||||||
| width: texture.width, | ||||||||||||||||||||||||||
| height: texture.height, | ||||||||||||||||||||||||||
| mipmapped: false) | ||||||||||||||||||||||||||
| texdescriptor.usage = [.shaderRead] | ||||||||||||||||||||||||||
| compositeCurrentTexture = compositeMtlDevice.makeTexture(descriptor: texdescriptor) | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| if compositeCommandQueue == nil { | ||||||||||||||||||||||||||
| compositeCommandQueue = compositeMtlDevice.makeCommandQueue() | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| guard let commandBuffer = compositeCommandQueue.makeCommandBuffer(), | ||||||||||||||||||||||||||
| let blitEncoder = commandBuffer.makeBlitCommandEncoder(), | ||||||||||||||||||||||||||
| let destTexture = compositeCurrentTexture else { return } | ||||||||||||||||||||||||||
| blitEncoder.copy(from: texture, | ||||||||||||||||||||||||||
| sourceSlice: 0, sourceLevel: 0, | ||||||||||||||||||||||||||
| sourceOrigin: MTLOrigin(x: 0, y: 0, z: 0), | ||||||||||||||||||||||||||
| sourceSize: MTLSizeMake(texture.width, texture.height, texture.depth), | ||||||||||||||||||||||||||
| to: destTexture, destinationSlice: 0, destinationLevel: 0, | ||||||||||||||||||||||||||
| destinationOrigin: MTLOrigin(x: 0, y: 0, z: 0)) | ||||||||||||||||||||||||||
| blitEncoder.endEncoding() | ||||||||||||||||||||||||||
| commandBuffer.commit() | ||||||||||||||||||||||||||
| commandBuffer.waitUntilCompleted() | ||||||||||||||||||||||||||
|
from2001 marked this conversation as resolved.
|
||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| if compositePointer == nil { | ||||||||||||||||||||||||||
| compositePointer = Unmanaged.passUnretained(compositeCurrentTexture!).toOpaque() | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| extension CVPixelBuffer { | ||||||||||||||||||||||||||
|
Comment on lines
+130
to
+135
|
||||||||||||||||||||||||||
| if compositePointer == nil { | |
| compositePointer = Unmanaged.passUnretained(compositeCurrentTexture!).toOpaque() | |
| } | |
| } | |
| extension CVPixelBuffer { | |
| compositePointer = Unmanaged.passUnretained(compositeCurrentTexture!).toOpaque() | |
| } | |
| extension CVPixelBuffer { | |
| extension CVPixelBuffer { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The isCompositeRunning flag is accessed from multiple threads (main thread in startCompositeCapture/stopCompositeCapture and ReplayKit's capture handler thread) without synchronization. This could lead to race conditions. Consider using proper thread synchronization mechanisms or atomic operations to ensure thread-safe access to this flag.