Skip to content

Commit

Permalink
Various Swift code improvements (#58)
Browse files Browse the repository at this point in the history
  • Loading branch information
sindresorhus authored Jul 12, 2018
1 parent 8a10d19 commit 0296d4b
Show file tree
Hide file tree
Showing 4 changed files with 261 additions and 46 deletions.
2 changes: 1 addition & 1 deletion index.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ class Aperture {

const recorderOpts = {
destination: fileUrl(this.tmpPath),
fps,
framesPerSecond: fps,
showCursor,
highlightClicks,
screenId,
Expand Down
5 changes: 2 additions & 3 deletions swift/aperture/Recorder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ final class Recorder: NSObject {
/// TODO: When targeting macOS 10.13, make the `videoCodec` option the type `AVVideoCodecType`
init(
destination: URL,
fps: Int,
framesPerSecond: Int,
cropRect: CGRect?,
showCursor: Bool,
highlightClicks: Bool,
Expand All @@ -42,8 +42,7 @@ final class Recorder: NSObject {

let input = AVCaptureScreenInput(displayID: screenId)

/// TODO: Use `CMTime(seconds:)` here instead
input.minFrameDuration = CMTime(value: 1, timescale: Int32(fps))
input.minFrameDuration = CMTime(videoFramesPerSecond: framesPerSecond)

if let cropRect = cropRect {
input.cropRect = cropRect
Expand Down
55 changes: 21 additions & 34 deletions swift/aperture/main.swift
Original file line number Diff line number Diff line change
@@ -1,18 +1,9 @@
import Foundation
import AVFoundation

var recorder: Recorder!
let arguments = CommandLine.arguments.dropFirst()

func quit(_: Int32) {
recorder.stop()
// Do not call `exit()` here as the video is not always done
// saving at this point and will be corrupted randomly
}

struct Options: Decodable {
let destination: URL
let fps: Int
let framesPerSecond: Int
let cropRect: CGRect?
let showCursor: Bool
let highlightClicks: Bool
Expand All @@ -22,12 +13,11 @@ struct Options: Decodable {
}

func record() throws {
let json = arguments.first!.data(using: .utf8)!
let options = try JSONDecoder().decode(Options.self, from: json)
let options: Options = try CLI.arguments.first!.jsonDecoded()

recorder = try Recorder(
let recorder = try Recorder(
destination: options.destination,
fps: options.fps,
framesPerSecond: options.framesPerSecond,
cropRect: options.cropRect,
showCursor: options.showCursor,
highlightClicks: options.highlightClicks,
Expand All @@ -45,22 +35,23 @@ func record() throws {
}

recorder.onError = {
printErr($0)
print($0, to: .standardError)
exit(1)
}

signal(SIGHUP, quit)
signal(SIGINT, quit)
signal(SIGTERM, quit)
signal(SIGQUIT, quit)
CLI.onExit = {
recorder.stop()
// Do not call `exit()` here as the video is not always done
// saving at this point and will be corrupted randomly
}

recorder.start()
setbuf(__stdoutp, nil)

setbuf(__stdoutp, nil)
RunLoop.main.run()
}

func usage() {
func showUsage() {
print(
"""
Usage:
Expand All @@ -71,21 +62,17 @@ func usage() {
)
}

if arguments.first == "list-screens" {
printErr(try toJson(DeviceList.screen()))
switch CLI.arguments.first {
case "list-screens":
print(try toJson(DeviceList.screen()), to: .standardError)
exit(0)
}

if arguments.first == "list-audio-devices" {
case "list-audio-devices":
// Uses stderr because of unrelated stuff being outputted on stdout
printErr(try toJson(DeviceList.audio()))
print(try toJson(DeviceList.audio()), to: .standardError)
exit(0)
}

if arguments.first != nil {
case .none:
showUsage()
exit(1)
default:
try record()
exit(0)
}

usage()
exit(1)
245 changes: 237 additions & 8 deletions swift/aperture/util.swift
Original file line number Diff line number Diff line change
@@ -1,26 +1,254 @@
import AppKit
import AVFoundation

private final class StandardErrorOutputStream: TextOutputStream {
func write(_ string: String) {
FileHandle.standardError.write(string.data(using: .utf8)!)

// MARK: - SignalHandler
struct SignalHandler {
struct Signal: Hashable {
static let hangup = Signal(rawValue: SIGHUP)
static let interrupt = Signal(rawValue: SIGINT)
static let quit = Signal(rawValue: SIGQUIT)
static let abort = Signal(rawValue: SIGABRT)
static let kill = Signal(rawValue: SIGKILL)
static let alarm = Signal(rawValue: SIGALRM)
static let termination = Signal(rawValue: SIGTERM)
static let userDefined1 = Signal(rawValue: SIGUSR1)
static let userDefined2 = Signal(rawValue: SIGUSR2)

/// Signals that cause the process to exit
static let exitSignals = [
hangup,
interrupt,
quit,
abort,
alarm,
termination
]

let rawValue: Int32
init(rawValue: Int32) {
self.rawValue = rawValue
}
}

typealias CSignalHandler = @convention(c) (Int32) -> Void
typealias SignalHandler = (Signal) -> Void

private static var handlers = [Signal: [SignalHandler]]()

private static var cHandler: CSignalHandler = { rawSignal in
let signal = Signal(rawValue: rawSignal)

guard let signalHandlers = handlers[signal] else {
return
}

for handler in signalHandlers {
handler(signal)
}
}

/// Handle some signals
static func handle(signals: [Signal], handler: @escaping SignalHandler) {
for signal in signals {
// Since Swift has no way of running code on "struct creation", we need to initialize here…
if handlers[signal] == nil {
handlers[signal] = []
}
handlers[signal]?.append(handler)

var signalAction = sigaction(
__sigaction_u: unsafeBitCast(cHandler, to: __sigaction_u.self),
sa_mask: 0,
sa_flags: 0
)

_ = withUnsafePointer(to: &signalAction) { pointer in
sigaction(signal.rawValue, pointer, nil)
}
}
}

/// Raise a signal
static func raise(signal: Signal) {
_ = Darwin.raise(signal.rawValue)
}

/// Ignore a signal
static func ignore(signal: Signal) {
_ = Darwin.signal(signal.rawValue, SIG_IGN)
}

/// Restore default signal handling
static func restore(signal: Signal) {
_ = Darwin.signal(signal.rawValue, SIG_DFL)
}
}

extension Array where Element == SignalHandler.Signal {
static let exitSignals = SignalHandler.Signal.exitSignals
}
// MARK: -


// MARK: - CLI utils
extension FileHandle: TextOutputStream {
public func write(_ string: String) {
write(string.data(using: .utf8)!)
}
}

struct CLI {
static var standardInput = FileHandle.standardOutput
static var standardOutput = FileHandle.standardOutput
static var standardError = FileHandle.standardError

static let arguments = Array(CommandLine.arguments.dropFirst(1))
}

extension CLI {
private static let once = Once()

/// Called when the process exits, either normally or forced (through signals)
/// When this is set, it's up to you to exit the process
static var onExit: (() -> Void)? {
didSet {
guard let exitHandler = onExit else {
return
}

let handler = {
once.run(exitHandler)
}

atexit_b {
handler()
}

SignalHandler.handle(signals: .exitSignals) { _ in
handler()
}
}
}

/// Called when the process is being forced (through signals) to exit
/// When this is set, it's up to you to exit the process
static var onForcedExit: ((SignalHandler.Signal) -> Void)? {
didSet {
guard let exitHandler = onForcedExit else {
return
}

SignalHandler.handle(signals: .exitSignals, handler: exitHandler)
}
}
}

private var stderr = StandardErrorOutputStream()
enum PrintOutputTarget {
case standardOutput
case standardError
}

func printErr(_ item: Any) {
print(item, to: &stderr)
/// Make `print()` accept an array of items
/// Since Swift doesn't support spreading...
private func print<Target>(
_ items: [Any],
separator: String = " ",
terminator: String = "\n",
to output: inout Target
) where Target: TextOutputStream {
let item = items.map { "\($0)" }.joined(separator: separator)
Swift.print(item, terminator: terminator, to: &output)
}

func print(
_ items: Any...,
separator: String = " ",
terminator: String = "\n",
to output: PrintOutputTarget = .standardOutput
) {
switch output {
case .standardOutput:
print(items, separator: separator, terminator: terminator)
case .standardError:
print(items, separator: separator, terminator: terminator, to: &CLI.standardError)
}
}
// MARK: -


// MARK: - Misc
func synchronized<T>(lock: AnyObject, closure: () throws -> T) rethrows -> T {
objc_sync_enter(lock)
defer {
objc_sync_exit(lock)
}

return try closure()
}

final class Once {
private var hasRun = false

/**
Executes the given closure only once (thread-safe)

```
final class Foo {
private let once = Once()

func bar() {
once.run {
print("Called only once")
}
}
}

let foo = Foo()
foo.bar()
foo.bar()
```
*/
func run(_ closure: () -> Void) {
synchronized(lock: self) {
guard !hasRun else {
return
}

hasRun = true
closure()
}
}
}

extension Data {
func jsonDecoded<T: Decodable>() throws -> T {
return try JSONDecoder().decode(T.self, from: self)
}
}

extension String {
func jsonDecoded<T: Decodable>() throws -> T {
return try data(using: .utf8)!.jsonDecoded()
}
}

func toJson<T>(_ data: T) throws -> String {
let json = try JSONSerialization.data(withJSONObject: data)
return String(data: json, encoding: .utf8)!
}

extension CMTimeScale {
static var video: CMTimeScale = 600 // This is what Apple recommends
}

extension CMTime {
static var zero: CMTime = kCMTimeZero
static var invalid: CMTime = kCMTimeInvalid
static let zero = kCMTimeZero
static let invalid = kCMTimeInvalid

init(videoFramesPerSecond: Int) {
self.init(seconds: 1 / Double(videoFramesPerSecond), preferredTimescale: .video)
}
}

extension CGDirectDisplayID {
Expand Down Expand Up @@ -77,3 +305,4 @@ extension NSScreen {
return name
}
}
// MARK: -

0 comments on commit 0296d4b

Please sign in to comment.