macOS: split AppMain into focused modules
This commit is contained in:
80
apps/macos/Sources/Clawdis/Screenshotter.swift
Normal file
80
apps/macos/Sources/Clawdis/Screenshotter.swift
Normal file
@@ -0,0 +1,80 @@
|
||||
import AppKit
|
||||
import CoreGraphics
|
||||
import Foundation
|
||||
@preconcurrency import ScreenCaptureKit
|
||||
import VideoToolbox
|
||||
|
||||
enum Screenshotter {
|
||||
@MainActor
|
||||
static func capture(displayID: UInt32?, windowID: UInt32?) async -> Data? {
|
||||
guard let content = try? await SCShareableContent.current else { return nil }
|
||||
|
||||
let targetDisplay: SCDisplay? = if let displayID {
|
||||
content.displays.first(where: { $0.displayID == displayID })
|
||||
} else {
|
||||
content.displays.first
|
||||
}
|
||||
|
||||
let filter: SCContentFilter
|
||||
if let windowID, let win = content.windows.first(where: { $0.windowID == windowID }) {
|
||||
filter = SCContentFilter(desktopIndependentWindow: win)
|
||||
} else if let display = targetDisplay {
|
||||
filter = SCContentFilter(display: display, excludingWindows: [])
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let config = SCStreamConfiguration()
|
||||
if let display = targetDisplay {
|
||||
config.width = display.width
|
||||
config.height = display.height
|
||||
}
|
||||
config.scalesToFit = true
|
||||
config.colorSpaceName = CGColorSpace.displayP3
|
||||
|
||||
let stream = SCStream(filter: filter, configuration: config, delegate: nil)
|
||||
let grabber = FrameGrabber()
|
||||
try? stream.addStreamOutput(
|
||||
grabber,
|
||||
type: .screen,
|
||||
sampleHandlerQueue: DispatchQueue(label: "com.steipete.clawdis.sshot"))
|
||||
do {
|
||||
try await stream.startCapture()
|
||||
let data = await grabber.awaitPNG()
|
||||
try? await stream.stopCapture()
|
||||
return data
|
||||
} catch {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final class FrameGrabber: NSObject, SCStreamOutput {
|
||||
private var continuation: CheckedContinuation<Data?, Never>?
|
||||
private var delivered = false
|
||||
|
||||
func awaitPNG() async -> Data? {
|
||||
await withCheckedContinuation { cont in
|
||||
self.continuation = cont
|
||||
}
|
||||
}
|
||||
|
||||
nonisolated func stream(
|
||||
_ stream: SCStream,
|
||||
didOutputSampleBuffer sampleBuffer: CMSampleBuffer,
|
||||
of outputType: SCStreamOutputType)
|
||||
{
|
||||
guard outputType == .screen else { return }
|
||||
if self.delivered { return }
|
||||
guard let imageBuffer = sampleBuffer.imageBuffer else { return }
|
||||
var cgImage: CGImage?
|
||||
let result = VTCreateCGImageFromCVPixelBuffer(imageBuffer, options: nil, imageOut: &cgImage)
|
||||
guard result == noErr, let cgImage else { return }
|
||||
let rep = NSBitmapImageRep(cgImage: cgImage)
|
||||
guard let data = rep.representation(using: .png, properties: [:]) else { return }
|
||||
|
||||
self.delivered = true
|
||||
self.continuation?.resume(returning: data)
|
||||
self.continuation = nil
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user