Skip to content
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

Rewrite using new Aperture and n-api #34

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ on:
jobs:
test:
name: Node.js ${{ matrix.node-version }}
runs-on: macos-14
runs-on: macos-15
strategy:
fail-fast: false
matrix:
Expand Down
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"),
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The branch here should change back to the real version when the Aperture PR is merged/released

.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
Loading