From b9881ac36df04e39148e3dbf5121bed1c528e8e6 Mon Sep 17 00:00:00 2001 From: jcm Date: Sun, 28 Apr 2024 22:53:38 -0500 Subject: [PATCH] application picker, miscellaneous changes --- CaptureSample/CaptureEngine.swift | 5 ++- CaptureSample/CaptureSampleApp.swift | 8 ++++ CaptureSample/ScreenRecorder.swift | 8 +++- .../Views/CaptureConfigurationOverlay.swift | 4 +- CaptureSample/Views/CapturePreview.swift | 4 +- .../AppControlsConfigurationView.swift | 41 +++++++------------ CaptureSample/Views/ContentView.swift | 1 + README.md | 4 +- .../RecordCameraExtensionProvider.swift | 7 +++- 9 files changed, 49 insertions(+), 33 deletions(-) diff --git a/CaptureSample/CaptureEngine.swift b/CaptureSample/CaptureEngine.swift index 4dd93d4..2579858 100644 --- a/CaptureSample/CaptureEngine.swift +++ b/CaptureSample/CaptureEngine.swift @@ -127,6 +127,7 @@ class CaptureEngineStreamOutput: NSObject, SCStreamOutput, SCStreamDelegate { var sink: RecordCameraStreamSink! = RecordCameraStreamSink() var sinkInitialized = false + var virtualCameraIsActive = false var framesWritten = 0 @@ -166,7 +167,9 @@ class CaptureEngineStreamOutput: NSObject, SCStreamOutput, SCStreamDelegate { } IOSurfaceLock(frame.surface!, [], nil) self.capturedFrameHandler?(frame) - self.sink.enqueue(frame.surface!) + if self.virtualCameraIsActive { + self.sink.enqueue(frame.surface!) + } IOSurfaceUnlock(frame.surface!, [], nil) } case .audio: diff --git a/CaptureSample/CaptureSampleApp.swift b/CaptureSample/CaptureSampleApp.swift index c6d8a78..51f0486 100644 --- a/CaptureSample/CaptureSampleApp.swift +++ b/CaptureSample/CaptureSampleApp.swift @@ -109,6 +109,14 @@ struct CaptureSampleApp: App { } } } + CommandMenu("Camera Extension") { + Button("Install Camera Extension...") { + self.screenRecorder.installExtension() + } + Button("Uninstall Camera Extension...") { + self.screenRecorder.uninstallExtension() + } + } } Window("Test Pattern", id: "testpattern") { TestPatternView(fps: $screenRecorder.framesPerSecond) diff --git a/CaptureSample/ScreenRecorder.swift b/CaptureSample/ScreenRecorder.swift index f9a3484..edaadf2 100644 --- a/CaptureSample/ScreenRecorder.swift +++ b/CaptureSample/ScreenRecorder.swift @@ -146,6 +146,11 @@ class ScreenRecorder: ObservableObject { @Published var isRunning = false @Published var isRecording = false @Published var isEncoding = false + @Published var virtualCameraIsActive = true { + didSet { + self.captureEngine.streamOutput.virtualCameraIsActive = virtualCameraIsActive + } + } @Published var captureWidth: Int = 0 @Published var captureHeight: Int = 0 @@ -616,7 +621,7 @@ class ScreenRecorder: ObservableObject { streamConfig.excludesCurrentProcessAudio = isAppAudioExcluded if #available(macOS 14.0, *) { //streamConfig.capturesShadowsOnly = true - //streamConfig.ignoreGlobalClipDisplay = true + streamConfig.ignoreGlobalClipDisplay = true } else { // Fallback on earlier versions } @@ -647,6 +652,7 @@ class ScreenRecorder: ObservableObject { // Increase the depth of the frame queue to ensure high fps at the expense of increasing // the memory footprint of WindowServer. streamConfig.queueDepth = 15 + streamConfig.backgroundColor = CGColor.clear return streamConfig } diff --git a/CaptureSample/Views/CaptureConfigurationOverlay.swift b/CaptureSample/Views/CaptureConfigurationOverlay.swift index 188e79b..d362f59 100644 --- a/CaptureSample/Views/CaptureConfigurationOverlay.swift +++ b/CaptureSample/Views/CaptureConfigurationOverlay.swift @@ -23,10 +23,10 @@ struct CaptureConfigurationOverlay: View { VStack { //GroupBox { Picker("Capture", selection: $screenRecorder.applicationFilterIsInclusive) { - Text("Inclusive") + Text("Exclude:") .tag(true) .font(.title) - Text("Exclusive") + Text("Include:") .tag(false) .font(.title) } diff --git a/CaptureSample/Views/CapturePreview.swift b/CaptureSample/Views/CapturePreview.swift index c31aa01..b6b9665 100644 --- a/CaptureSample/Views/CapturePreview.swift +++ b/CaptureSample/Views/CapturePreview.swift @@ -44,13 +44,14 @@ struct CaptureSplitViewPreview: NSViewRepresentable { init() { //contentLayer.contentsGravity = .resizeAspect contentLayer.contentsGravity = .topLeft + contentLayer.backgroundColor = CGColor.clear encodedContentLayer.contentsGravity = .topRight self.renderer = encodedContentLayer } func makeNSView(context: Context) -> CaptureSplitViewPreview { //CaptureVideoPreview(layer: contentLayer) - CaptureSplitViewPreview(firstLayer: contentLayer, secondLayer: encodedContentLayer) + return CaptureSplitViewPreview(firstLayer: contentLayer, secondLayer: encodedContentLayer) } // Called by ScreenRecorder as it receives new video frames. @@ -79,6 +80,7 @@ struct CaptureSplitViewPreview: NSViewRepresentable { init(firstLayer: CALayer, secondLayer: CALayer) { self.firstView = CaptureVideoPreview(layer: firstLayer) self.secondView = CaptureVideoPreview(layer: secondLayer) + self.firstView.layer?.backgroundColor = CGColor.clear super.init(frame: .zero) self.isVertical = true self.addSubview(self.firstView) diff --git a/CaptureSample/Views/Configuration View/AppControlsConfigurationView.swift b/CaptureSample/Views/Configuration View/AppControlsConfigurationView.swift index 94c83e3..f1edb9f 100644 --- a/CaptureSample/Views/Configuration View/AppControlsConfigurationView.swift +++ b/CaptureSample/Views/Configuration View/AppControlsConfigurationView.swift @@ -41,25 +41,25 @@ struct AppControlsConfigurationView: View { } Spacer(minLength: 10) HStack { - if !screenRecorder.isRecording { + if screenRecorder.usesReplayBuffer && screenRecorder.isRecording { Button { - Task { await screenRecorder.record() } + Task { screenRecorder.saveReplayBuffer() } } label: { - Text("Start Recording") + Text("Save Replay Buffer") } .controlSize(.large) .buttonStyle(.borderedProminent) - .disabled(screenRecorder.isRecording || !screenRecorder.isRunning) + .disabled(!screenRecorder.isRecording || !screenRecorder.isRunning) } - if screenRecorder.usesReplayBuffer && screenRecorder.isRecording { + if !screenRecorder.isRecording { Button { - Task { screenRecorder.saveReplayBuffer() } + Task { await screenRecorder.record() } } label: { - Text("Save Replay Buffer") + Text("Start Recording") } .controlSize(.large) .buttonStyle(.borderedProminent) - .disabled(!screenRecorder.isRecording || !screenRecorder.isRunning) + .disabled(screenRecorder.isRecording || !screenRecorder.isRunning) } if screenRecorder.isRecording { Button { @@ -74,30 +74,19 @@ struct AppControlsConfigurationView: View { } .frame(maxWidth: .infinity) .padding(EdgeInsets(top: 0, leading: 0, bottom: 15, trailing: 0)) - HStack { + if !screenRecorder.virtualCameraIsActive { Button { - Task { await screenRecorder.uninstallExtension() } + Task { screenRecorder.virtualCameraIsActive = true } } label: { - Text("Remove Extension") + Text("Start Virtual Camera") } - .controlSize(.large) - .buttonStyle(.borderedProminent) - Button { - Task { screenRecorder.installExtension() } - } label: { - Text("Install Extension") - } - .controlSize(.large) - .buttonStyle(.borderedProminent) + } + if screenRecorder.virtualCameraIsActive { Button { - Task { screenRecorder.testSetProperty() } + Task { screenRecorder.virtualCameraIsActive = false } } label: { - Text("Test Set Property") + Text("Stop Virtual Camera") } - .controlSize(.large) - .buttonStyle(.borderedProminent) } - .frame(maxWidth: .infinity) - .padding(EdgeInsets(top: 0, leading: 0, bottom: 15, trailing: 0)) } } diff --git a/CaptureSample/Views/ContentView.swift b/CaptureSample/Views/ContentView.swift index af0d1ad..36c7f3d 100644 --- a/CaptureSample/Views/ContentView.swift +++ b/CaptureSample/Views/ContentView.swift @@ -103,6 +103,7 @@ struct ContentView: View { overlayOpacity = hover == true ? 1.0 : 0.0 } } + .background(.white) } } .navigationTitle("Record") diff --git a/README.md b/README.md index 15d4b5b..27e5c66 100644 --- a/README.md +++ b/README.md @@ -21,9 +21,11 @@ Record is in active development and uses rolling releases. Download the most rec # Roadmap +* fixup virtual camera, implement install flow for CMIO extension * menu bar mode / run in background +* better test pattern +* stable builds / unbreak CI * audio settings / audio metering / audio only -* advanced capture options (include some apps, not others) * compositing? * streaming? diff --git a/RecordCameraExtension/RecordCameraExtensionProvider.swift b/RecordCameraExtension/RecordCameraExtensionProvider.swift index c94c5f3..28f52dc 100644 --- a/RecordCameraExtension/RecordCameraExtensionProvider.swift +++ b/RecordCameraExtension/RecordCameraExtensionProvider.swift @@ -233,7 +233,12 @@ class RecordCameraExtensionDeviceSource: NSObject, CMIOExtensionDeviceSource { self.lastTimingInfo.presentationTimeStamp = CMClockGetTime(CMClockGetHostTimeClock()) let output: CMIOExtensionScheduledOutput = CMIOExtensionScheduledOutput(sequenceNumber: seq, hostTimeInNanoseconds: UInt64(self.lastTimingInfo.presentationTimeStamp.seconds * Double(NSEC_PER_SEC))) if self._streamingCounter > 0 { - self._streamSource.stream.send(sbuf!, discontinuity: [], hostTimeInNanoseconds: UInt64(sbuf!.presentationTimeStamp.seconds * Double(NSEC_PER_SEC))) + var newSbuf: CMSampleBuffer? + var timingInfo = CMSampleTimingInfo() + timingInfo.presentationTimeStamp = CMClockGetTime(CMClockGetHostTimeClock()) + var err: OSStatus = 0 + err = CMSampleBufferCreateCopyWithNewTiming(allocator: kCFAllocatorDefault, sampleBuffer: sbuf!, sampleTimingEntryCount: 1, sampleTimingArray: &timingInfo, sampleBufferOut: &newSbuf) + self._streamSource.stream.send(newSbuf!, discontinuity: [], hostTimeInNanoseconds: UInt64(newSbuf!.presentationTimeStamp.seconds * Double(NSEC_PER_SEC))) } self._streamSink.stream.notifyScheduledOutputChanged(output) if let surface = CVPixelBufferGetIOSurface(sbuf?.imageBuffer)?.takeUnretainedValue() {