Skip to content

Commit

Permalink
Rewrite using new Aperture and n-api
Browse files Browse the repository at this point in the history
  • Loading branch information
karaggeorge committed Nov 11, 2024
1 parent 00c8b35 commit 004ba5d
Show file tree
Hide file tree
Showing 16 changed files with 1,102 additions and 160 deletions.
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,10 @@ xcuserdata
/Packages
/*.xcodeproj
/aperture
/aperture.node

recording.mp4


# SwiftLint Remote Config Cache
.swiftlint/RemoteConfigCache
6 changes: 6 additions & 0 deletions .swiftlint.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
parent_config: https://raw.githubusercontent.com/sindresorhus/swiftlint-config/main/.swiftlint.yml
deployment_target:
macOS_deployment_target: '13'
excluded:
- .build
- node_modules
17 changes: 13 additions & 4 deletions Package.resolved
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,26 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/wulkano/Aperture",
"state" : {
"revision" : "ddfb0fc1b3c789339dd5fd9296ba8076d292611c",
"version" : "2.0.1"
"branch" : "george/rewrite-in-screen-capture-kit",
"revision" : "6a3adffa8b3af3fd766e581bddf2c4416bf4547a"
}
},
{
"identity" : "swift-argument-parser",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-argument-parser",
"state" : {
"revision" : "46989693916f56d1186bd59ac15124caef896560",
"version" : "1.3.1"
"revision" : "41982a3656a71c768319979febd796c6fd111d5c",
"version" : "1.5.0"
}
},
{
"identity" : "swift-syntax",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-syntax.git",
"state" : {
"revision" : "0687f71944021d616d34d922343dcef086855920",
"version" : "600.0.1"
}
}
],
Expand Down
20 changes: 17 additions & 3 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,25 @@ import PackageDescription
let package = Package(
name: "ApertureCLI",
platforms: [
.macOS(.v10_13)
.macOS(.v13)
],
products: [
.executable(
name: "aperture",
targets: [
"ApertureCLI"
]
),
.library(
name: "aperture-module",
type: .dynamic,
targets: ["ApertureModule"]
)
],
dependencies: [
.package(url: "https://github.com/wulkano/Aperture", from: "2.0.1"),
.package(url: "https://github.com/apple/swift-argument-parser", from: "1.3.1")
.package(url: "https://github.com/wulkano/Aperture", branch: "george/rewrite-in-screen-capture-kit"),
.package(url: "https://github.com/apple/swift-argument-parser", from: "1.3.1"),
.package(path: "node_modules/node-swift")
],
targets: [
.executableTarget(
Expand All @@ -25,6 +31,14 @@ let package = Package(
"Aperture",
.product(name: "ArgumentParser", package: "swift-argument-parser")
]
),
.target(
name: "ApertureModule",
dependencies: [
"Aperture",
.product(name: "NodeAPI", package: "node-swift"),
.product(name: "NodeModuleSupport", package: "node-swift")
]
)
]
)
97 changes: 93 additions & 4 deletions Sources/ApertureCLI/ApertureCLI.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,13 @@ enum OutEvent: String, CaseIterable, ExpressibleByArgument {
case onFinish
}

enum TargetType: String, CaseIterable, ExpressibleByArgument {
case screen
case window
case audio
case externalDevice
}

enum InEvent: String, CaseIterable, ExpressibleByArgument {
case pause
case resume
Expand Down Expand Up @@ -40,7 +47,9 @@ extension ApertureCLI {
static let configuration = CommandConfiguration(
subcommands: [
Screens.self,
AudioDevices.self
AudioDevices.self,
Windows.self,
ExternalDevices.self
]
)
}
Expand All @@ -51,11 +60,23 @@ extension ApertureCLI {
@Option(name: .shortAndLong, help: "The ID to use for this process")
var processId = "main"

@Option(name: .shortAndLong, help: "The type of target to record")
var targetType = TargetType.screen

@Argument(help: "Stringified JSON object with options passed to Aperture")
var options: String

mutating func run() throws {
try record(options, processId: processId)
Task { [self] in
do {
try await record(options, processId: processId, targetType: targetType)
} catch {
print(error, to: .standardError)
Darwin.exit(1)
}
}

RunLoop.main.run()
}
}

Expand All @@ -75,8 +96,67 @@ extension ApertureCLI.List {
static let configuration = CommandConfiguration(abstract: "List available screens.")

mutating func run() throws {
// Uses stderr because of unrelated stuff being outputted on stdout.
print(try toJson(Aperture.Devices.screen().map { ["id": $0.id, "name": $0.name] }), to: .standardError)
Task {
// Uses stderr because of unrelated stuff being outputted on stdout.
print(
try toJson(
await Aperture.Devices.screen().map {
[
"id": $0.id,
"name": $0.name,
"width": $0.width,
"height": $0.height,
"frame": $0.frame.asDictionary
]
}
),
to: .standardError
)
Darwin.exit(0)
}

RunLoop.main.run()
}
}

struct Windows: ParsableCommand {
static let configuration = CommandConfiguration(abstract: "List available windows.")

@Flag(inversion: .prefixedNo, help: "Exclude desktop windows")
var excludeDesktopWindows = true

@Flag(inversion: .prefixedNo, help: "Only include windows that are on screen")
var onScreenOnly = true

mutating func run() throws {
Task { [self] in
// Uses stderr because of unrelated stuff being outputted on stdout.
print(
try toJson(
await Aperture.Devices.window(
excludeDesktopWindows: excludeDesktopWindows,
onScreenWindowsOnly: onScreenOnly
)
.map {
[
"id": $0.id,
"title": $0.title as Any,
"applicationName": $0.applicationName as Any,
"applicationBundleIdentifier": $0.applicationBundleIdentifier as Any,
"isActive": $0.isActive,
"isOnScreen": $0.isOnScreen,
"layer": $0.layer,
"frame": $0.frame.asDictionary
]
}
),
to: .standardError
)

Darwin.exit(0)
}

RunLoop.main.run()
}
}

Expand All @@ -88,6 +168,15 @@ extension ApertureCLI.List {
print(try toJson(Aperture.Devices.audio().map { ["id": $0.id, "name": $0.name] }), to: .standardError)
}
}

struct ExternalDevices: ParsableCommand {
static let configuration = CommandConfiguration(abstract: "List available external devices.")

mutating func run() throws {
// Uses stderr because of unrelated stuff being outputted on stdout.
print(try toJson(Aperture.Devices.iOS().map { ["id": $0.id, "name": $0.name] }), to: .standardError)
}
}
}

extension ApertureCLI.Events {
Expand Down
11 changes: 11 additions & 0 deletions Sources/ApertureCLI/Utilities.swift
Original file line number Diff line number Diff line change
Expand Up @@ -260,3 +260,14 @@ func toJson<T>(_ data: T) throws -> String {
return String(data: json, encoding: .utf8)!
}
// MARK: -

extension CGRect {
var asDictionary: [String: Any] {
[
"x": Int(origin.x),
"y": Int(origin.y),
"width": Int(size.width),
"height": Int(size.height)
]
}
}
75 changes: 49 additions & 26 deletions Sources/ApertureCLI/record.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,30 +3,22 @@ import Aperture

struct Options: Decodable {
let destination: URL
let targetId: String?
let framesPerSecond: Int
let cropRect: CGRect?
let showCursor: Bool
let highlightClicks: Bool
let screenId: CGDirectDisplayID
let audioDeviceId: String?
let videoCodec: String?
let losslessAudio: Bool
let recordSystemAudio: Bool
}

func record(_ optionsString: String, processId: String) throws {
setbuf(__stdoutp, nil)
func record(_ optionsString: String, processId: String, targetType: TargetType) async throws {
let options: Options = try optionsString.jsonDecoded()
var observers = [Any]()

let recorder = try Aperture(
destination: options.destination,
framesPerSecond: options.framesPerSecond,
cropRect: options.cropRect,
showCursor: options.showCursor,
highlightClicks: options.highlightClicks,
screenId: options.screenId == 0 ? .main : options.screenId,
audioDevice: options.audioDeviceId != nil ? AVCaptureDevice(uniqueID: options.audioDeviceId!) : nil,
videoCodec: options.videoCodec != nil ? AVVideoCodecType(rawValue: options.videoCodec!) : nil
)
let recorder = Aperture.Recorder()

recorder.onStart = {
ApertureEvents.sendEvent(processId: processId, event: OutEvent.onFileReady.rawValue)
Expand All @@ -40,16 +32,12 @@ func record(_ optionsString: String, processId: String) throws {
ApertureEvents.sendEvent(processId: processId, event: OutEvent.onResume.rawValue)
}

recorder.onFinish = {
switch $0 {
case .success(_):
// TODO: Handle warning on the JS side.
break
case .failure(let error):
print(error, to: .standardError)
exit(1)
}
recorder.onError = {
print($0, to: .standardError)
exit(1)
}

recorder.onFinish = {
ApertureEvents.sendEvent(processId: processId, event: OutEvent.onFinish.rawValue)

for observer in observers {
Expand All @@ -60,7 +48,9 @@ func record(_ optionsString: String, processId: String) throws {
}

CLI.onExit = {
recorder.stop()
Task {
try await recorder.stopRecording()
}
// Do not call `exit()` here as the video is not always done
// saving at this point and will be corrupted randomly
}
Expand All @@ -83,8 +73,41 @@ func record(_ optionsString: String, processId: String) throws {
}
)

recorder.start()
ApertureEvents.sendEvent(processId: processId, event: OutEvent.onStart.rawValue)
let videoCodec: Aperture.VideoCodec
if let videoCodecString = options.videoCodec {
videoCodec = try .fromRawValue(videoCodecString)
} else {
videoCodec = .h264
}

let target: Aperture.Target

switch targetType {
case .screen:
target = .screen
case .window:
target = .window
case .audio:
target = .audioOnly
case .externalDevice:
target = .externalDevice
}

RunLoop.main.run()
try await recorder.startRecording(
target: target,
options: Aperture.RecordingOptions(
destination: options.destination,
targetID: options.targetId,
framesPerSecond: options.framesPerSecond,
cropRect: options.cropRect,
showCursor: options.showCursor,
highlightClicks: options.highlightClicks,
videoCodec: videoCodec,
losslessAudio: options.losslessAudio,
recordSystemAudio: options.recordSystemAudio,
microphoneDeviceID: options.audioDeviceId != nil ? options.audioDeviceId : nil
)
)

ApertureEvents.sendEvent(processId: processId, event: OutEvent.onStart.rawValue)
}
Loading

0 comments on commit 004ba5d

Please sign in to comment.