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 with ScreenCaptureKit #80

Open
wants to merge 10 commits into
base: main
Choose a base branch
from

Conversation

karaggeorge
Copy link
Member

@karaggeorge karaggeorge commented Nov 10, 2024

Rewrites Aperture to use ScreenCaptureKit

Breaking changes:

  • Now requires macOS 13+
  • highlightClicks is only available on macOS 15+
  • Aperture itself is not the recorder, you can instantiate Aperture.Recorder() and use startRecording() and stopRecording() on it
  • Everything uses async/await instead of callbacks
  • startRecording awaits until the file actually starts getting written before resolving, or throws any errors that happen during that time
  • cropRect is top/left now (this is how ScreenCaptureKit works by default)

This has as much feature parity as possible (other then the highlightClicks note above) but it adds some functionality as well:

  • Also supports .mov and .m4v for videos
  • Recording system audio (+ mic)
  • Loseless audio option
  • Recording audio only (only .m4a for now, AAC or ALAC encoding)
  • Recording a specific window (will follow the window around even if it moves or covered by other windows)

Potential features we'd want to add in a follow-up:

  • Support more audio formats
  • Maybe support quality/bitrate options for audio/video
  • Support recording applications as targets
  • Support excluding apps/windows from the recording (for screen/application only)
    • Some good default sets would be excluding the current application itself and/or desktop icons

This PR still needs to update the readme, but wanted to get it reviewed first to fully land on the implementation before I go and update the readme

Closes #16
Closes #31
Closes #71
Closes #63
Closes #65


IssueHunt Summary

Referenced issues

This pull request has been submitted to:


@@ -1,228 +1,662 @@
import Foundation
import AVFoundation
import ScreenCaptureKit

public final class Aperture {
Copy link
Contributor

Choose a reason for hiding this comment

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

Since this is simply a namespace, make it enum instead.

public struct RecordingOptions {
public init(
destination: URL,
targetId: String? = nil,
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
targetId: String? = nil,
targetID: String? = nil,

to match Swift style. Applies in other places too.

Copy link
Member Author

@karaggeorge karaggeorge Nov 10, 2024

Choose a reason for hiding this comment

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

Thanks! These are great. Took some courses on Swift before I wrote this to get a better understanding, but they don't really cover formatting

Is there something like eslint/prettier for Swift that can help me identify these? (and things like not adding : Bool if there's a default value etc)

Using XCode fwiw, but I can use VSCode too if there's something available there

Copy link
Contributor

Choose a reason for hiding this comment

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

case audioOnly
}

public enum ApertureError: Swift.Error {
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
public enum ApertureError: Swift.Error {
public enum Error: Swift.Error {

case noTargetProvided
case invalidFileExtension(String, Bool)
case noDisplaysConnected
case unknownError(Swift.Error)
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
case unknownError(Swift.Error)
case unknown(Swift.Error)

And should be last.

Comment on lines 93 to 95
private var isRunning: Bool = false
/// Whether the recorder is paused
private var isPaused: Bool = false
Copy link
Contributor

Choose a reason for hiding this comment

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

I feel like these would be better as a single enum. Maybe isStreamRecording should also be included.

Copy link
Member Author

Choose a reason for hiding this comment

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

How can I do this with an enum? Do you mean struct to hold each var? They can have independent values at times

Copy link
Contributor

Choose a reason for hiding this comment

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

Can it be isRunning == true and isPaused == true at the same time?

externalDeviceCaptureSession.stopRunning()
}

if (assetWriter?.status == .writing) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
if (assetWriter?.status == .writing) {
if assetWriter?.status == .writing {

audioDevice: audioDevice,
videoCodec: videoCodec
)
extension Aperture.Recorder: SCStreamDelegate, SCStreamOutput, AVCaptureAudioDataOutputSampleBufferDelegate, AVCaptureVideoDataOutputSampleBufferDelegate {
Copy link
Contributor

Choose a reason for hiding this comment

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

If possible, one extension per conformance. Makes it easier to read.

private func initOutput(target: Aperture.Target, options: Aperture.RecordingOptions, streamConfig: SCStreamConfiguration) async throws {
let assetWriter = try getAssetWriter(target: target, options: options)

var audioSettings: [String: Any] = [AVSampleRateKey : 48000, AVNumberOfChannelsKey : 2]
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
var audioSettings: [String: Any] = [AVSampleRateKey : 48000, AVNumberOfChannelsKey : 2]
var audioSettings: [String: Any] = [AVSampleRateKey: 48000, AVNumberOfChannelsKey: 2]

case unknownError(Swift.Error)
case noPermissions
}
public final class Recorder: NSObject {
Copy link
Contributor

Choose a reason for hiding this comment

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

Define this in an extension for improved readability.


/// Internal helpers for when we are resuming, used to fix the buffer timing
private var isResuming: Bool = false
private var timeOffset: CMTime = .zero
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
private var timeOffset: CMTime = .zero
private var timeOffset = CMTime.zero


service = IOIteratorNext(iterator)
extension Aperture {
public static func hasPermissions() async -> Bool {
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
public static func hasPermissions() async -> Bool {
public static var hasPermissions: Bool {
get async {

return "Unnamed screen"
extension CMSampleBuffer {
public func adjustTime(by offset: CMTime) -> CMSampleBuffer? {
guard CMSampleBufferGetFormatDescription(self) != nil else { return nil }
Copy link
Contributor

Choose a reason for hiding this comment

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

Use self.formatDescription. Same with the other things.

@karaggeorge
Copy link
Member Author

@sindresorhus addressed all except for this and configured/ran swiftlint 😌

@karaggeorge
Copy link
Member Author

While working on aperture-node with this version, I noticed a few things missing. Pushed some commits above, but need to also add:

  • Support for proRes codecs
  • Error handling if we can't add the video inputs (we silently fail right now, took me a while to figure out why prores wasn't working)

will be pushing a commit with those two at some point tomorrow

@karaggeorge
Copy link
Member Author

Ok, I think it's ready:

  • Added support for proRes
    • Created a scoped enum for the ones we support and an extension to map them
    • Added checks for invalid file extensions <-> codec
  • Added better error handling
    • Fixed some errors to have clearer message
    • Fixed logging for error sub-reasons
    • Throw errors if the stream does not start instead of failing silently or crashing
  • Added a reset to the class so it can be re-used after recording is done

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
2 participants