Keep window crops aligned as windows change

This commit is contained in:
young
2026-09-04 20:06:05 +10:00
parent ed32f05f52
commit a5bbfa8e43
10 changed files with 150 additions and 42 deletions
+60 -10
View File
@@ -40,6 +40,8 @@ final class ScreenCaptureRecorder: NSObject, SCStreamOutput, SCStreamDelegate {
private var videoInput: AVAssetWriterInput?
private var videoPixelBufferAdaptor: AVAssetWriterInputPixelBufferAdaptor?
private var windowCropRect: CGRect?
private var windowCropDisplayId: CGDirectDisplayID?
private var excludedProcessIds = Set<Int32>()
private var lastCroppedPixelBuffer: CVPixelBuffer?
private let imageContext = CIContext(options: [.cacheIntermediates: false])
private var systemAudioWriter: AVAssetWriter?
@@ -123,7 +125,7 @@ final class ScreenCaptureRecorder: NSObject, SCStreamOutput, SCStreamDelegate {
let filter: SCContentFilter
let outputWidth: Int
let outputHeight: Int
let excludedProcessIds = Set(config.excludedProcessIds ?? [])
excludedProcessIds = Set(config.excludedProcessIds ?? [])
let excludedApplications = availableContent.applications.filter {
excludedProcessIds.contains($0.processID)
}
@@ -134,14 +136,7 @@ final class ScreenCaptureRecorder: NSObject, SCStreamOutput, SCStreamDelegate {
throw NSError(domain: "RecordlyCapture", code: 3, userInfo: [NSLocalizedDescriptionKey: "Window not found"])
}
guard let display = availableContent.displays.first(where: {
$0.frame.intersects(window.frame) || $0.frame.contains(CGPoint(x: window.frame.midX, y: window.frame.midY))
}) else {
throw NSError(domain: "RecordlyCapture", code: 4, userInfo: [NSLocalizedDescriptionKey: "Window display not found"])
}
// Accessibility reports the visible frame at the native border.
let scaleFactor = ScreenCaptureRecorder.scaleFactor(for: display.displayID)
let visibleFrame: CGRect
if let x = config.windowX,
let y = config.windowY,
@@ -153,6 +148,10 @@ final class ScreenCaptureRecorder: NSObject, SCStreamOutput, SCStreamDelegate {
} else {
visibleFrame = window.frame
}
guard let display = Self.captureDisplay(for: visibleFrame, from: availableContent.displays) else {
throw NSError(domain: "RecordlyCapture", code: 4, userInfo: [NSLocalizedDescriptionKey: "Window display not found"])
}
let scaleFactor = ScreenCaptureRecorder.scaleFactor(for: display.displayID)
let captureRect = visibleFrame.intersection(display.frame)
filter = SCContentFilter(
display: display,
@@ -165,6 +164,7 @@ final class ScreenCaptureRecorder: NSObject, SCStreamOutput, SCStreamDelegate {
width: captureRect.width / display.frame.width,
height: captureRect.height / display.frame.height
)
windowCropDisplayId = display.displayID
outputWidth = max(2, Int(captureRect.width) * scaleFactor) & ~1
outputHeight = max(2, Int(captureRect.height) * scaleFactor) & ~1
streamConfig.width = max(2, Int(display.frame.width) * scaleFactor)
@@ -173,6 +173,7 @@ final class ScreenCaptureRecorder: NSObject, SCStreamOutput, SCStreamDelegate {
} else {
trackedWindowId = nil
windowCropRect = nil
windowCropDisplayId = nil
let displayId = config.displayId ?? CGMainDisplayID()
guard let display = availableContent.displays.first(where: { $0.displayID == displayId }) else {
throw NSError(domain: "RecordlyCapture", code: 4, userInfo: [NSLocalizedDescriptionKey: "Display not found"])
@@ -637,6 +638,8 @@ final class ScreenCaptureRecorder: NSObject, SCStreamOutput, SCStreamDelegate {
videoInput = nil
videoPixelBufferAdaptor = nil
windowCropRect = nil
windowCropDisplayId = nil
excludedProcessIds.removeAll()
lastCroppedPixelBuffer = nil
systemAudioWriter = nil
systemAudioInput = nil
@@ -869,8 +872,7 @@ final class ScreenCaptureRecorder: NSObject, SCStreamOutput, SCStreamDelegate {
continue
}
let windowStillAvailable = availableContent.windows.contains(where: { $0.windowID == trackedWindowId })
if !windowStillAvailable {
guard let window = availableContent.windows.first(where: { $0.windowID == trackedWindowId }) else {
print("WINDOW_UNAVAILABLE")
fflush(stdout)
let finalization = await self.finalizeCapture(interactive: false)
@@ -887,11 +889,59 @@ final class ScreenCaptureRecorder: NSObject, SCStreamOutput, SCStreamDelegate {
fflush(stderr)
exit(1)
}
return
}
guard let display = Self.captureDisplay(for: window.frame, from: availableContent.displays) else {
continue
}
let captureRect = window.frame.intersection(display.frame)
guard captureRect.width > 0, captureRect.height > 0 else { continue }
let cropRect = CGRect(
x: (captureRect.minX - display.frame.minX) / display.frame.width,
y: (captureRect.minY - display.frame.minY) / display.frame.height,
width: captureRect.width / display.frame.width,
height: captureRect.height / display.frame.height
)
if self.windowCropDisplayId != display.displayID, let activeStream = self.stream {
let excludedApplications = availableContent.applications.filter {
self.excludedProcessIds.contains($0.processID)
}
let filter = SCContentFilter(
display: display,
excludingApplications: excludedApplications,
exceptingWindows: []
)
do {
try await activeStream.updateContentFilter(filter)
} catch {
continue
}
}
await withCheckedContinuation { (continuation: CheckedContinuation<Void, Never>) in
self.queue.async {
if self.isRecording {
self.windowCropRect = cropRect
self.windowCropDisplayId = display.displayID
}
continuation.resume()
}
}
}
}
}
private static func captureDisplay(for frame: CGRect, from displays: [SCDisplay]) -> SCDisplay? {
let midpoint = CGPoint(x: frame.midX, y: frame.midY)
return displays.first(where: { $0.frame.contains(midpoint) })
?? displays.filter { $0.frame.intersects(frame) }.max {
$0.frame.intersection(frame).width * $0.frame.intersection(frame).height
< $1.frame.intersection(frame).width * $1.frame.intersection(frame).height
}
}
private static func scaleFactor(for displayId: CGDirectDisplayID) -> Int {
guard let mode = CGDisplayCopyDisplayMode(displayId) else {
return 1