Various Swift code improvements (#58)

This commit is contained in:
Sindre Sorhus 2018-07-12 17:44:13 +07:00
parent e9184b38a3
commit e5d4ddf60f
3 changed files with 260 additions and 45 deletions

View File

@ -29,7 +29,7 @@ final class Recorder: NSObject {
/// TODO: When targeting macOS 10.13, make the `videoCodec` option the type `AVVideoCodecType` /// TODO: When targeting macOS 10.13, make the `videoCodec` option the type `AVVideoCodecType`
init( init(
destination: URL, destination: URL,
fps: Int, framesPerSecond: Int,
cropRect: CGRect?, cropRect: CGRect?,
showCursor: Bool, showCursor: Bool,
highlightClicks: Bool, highlightClicks: Bool,
@ -42,8 +42,7 @@ final class Recorder: NSObject {
let input = AVCaptureScreenInput(displayID: screenId) let input = AVCaptureScreenInput(displayID: screenId)
/// TODO: Use `CMTime(seconds:)` here instead input.minFrameDuration = CMTime(videoFramesPerSecond: framesPerSecond)
input.minFrameDuration = CMTime(value: 1, timescale: Int32(fps))
if let cropRect = cropRect { if let cropRect = cropRect {
input.cropRect = cropRect input.cropRect = cropRect

View File

@ -1,18 +1,9 @@
import Foundation import Foundation
import AVFoundation 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 { struct Options: Decodable {
let destination: URL let destination: URL
let fps: Int let framesPerSecond: Int
let cropRect: CGRect? let cropRect: CGRect?
let showCursor: Bool let showCursor: Bool
let highlightClicks: Bool let highlightClicks: Bool
@ -22,12 +13,11 @@ struct Options: Decodable {
} }
func record() throws { func record() throws {
let json = arguments.first!.data(using: .utf8)! let options: Options = try CLI.arguments.first!.jsonDecoded()
let options = try JSONDecoder().decode(Options.self, from: json)
recorder = try Recorder( let recorder = try Recorder(
destination: options.destination, destination: options.destination,
fps: options.fps, framesPerSecond: options.framesPerSecond,
cropRect: options.cropRect, cropRect: options.cropRect,
showCursor: options.showCursor, showCursor: options.showCursor,
highlightClicks: options.highlightClicks, highlightClicks: options.highlightClicks,
@ -45,22 +35,23 @@ func record() throws {
} }
recorder.onError = { recorder.onError = {
printErr($0) print($0, to: .standardError)
exit(1) exit(1)
} }
signal(SIGHUP, quit) CLI.onExit = {
signal(SIGINT, quit) recorder.stop()
signal(SIGTERM, quit) // Do not call `exit()` here as the video is not always done
signal(SIGQUIT, quit) // saving at this point and will be corrupted randomly
}
recorder.start() recorder.start()
setbuf(__stdoutp, nil)
setbuf(__stdoutp, nil)
RunLoop.main.run() RunLoop.main.run()
} }
func usage() { func showUsage() {
print( print(
""" """
Usage: Usage:
@ -71,21 +62,17 @@ func usage() {
) )
} }
if arguments.first == "list-screens" { switch CLI.arguments.first {
printErr(try toJson(DeviceList.screen())) case "list-screens":
print(try toJson(DeviceList.screen()), to: .standardError)
exit(0) exit(0)
} case "list-audio-devices":
if arguments.first == "list-audio-devices" {
// Uses stderr because of unrelated stuff being outputted on stdout // Uses stderr because of unrelated stuff being outputted on stdout
printErr(try toJson(DeviceList.audio())) print(try toJson(DeviceList.audio()), to: .standardError)
exit(0) exit(0)
} case .none:
showUsage()
if arguments.first != nil { exit(1)
default:
try record() try record()
exit(0)
} }
usage()
exit(1)

View File

@ -1,16 +1,236 @@
import AppKit import AppKit
import AVFoundation import AVFoundation
private final class StandardErrorOutputStream: TextOutputStream {
func write(_ string: String) { // MARK: - SignalHandler
FileHandle.standardError.write(string.data(using: .utf8)!) 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)
} }
} }
private var stderr = StandardErrorOutputStream() extension Array where Element == SignalHandler.Signal {
static let exitSignals = SignalHandler.Signal.exitSignals
}
// MARK: -
func printErr(_ item: Any) {
print(item, to: &stderr) // 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)
}
}
}
enum PrintOutputTarget {
case standardOutput
case standardError
}
/// 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 { func toJson<T>(_ data: T) throws -> String {
@ -18,9 +238,17 @@ func toJson<T>(_ data: T) throws -> String {
return String(data: json, encoding: .utf8)! return String(data: json, encoding: .utf8)!
} }
extension CMTimeScale {
static var video: CMTimeScale = 600 // This is what Apple recommends
}
extension CMTime { extension CMTime {
static var zero: CMTime = kCMTimeZero static let zero = kCMTimeZero
static var invalid: CMTime = kCMTimeInvalid static let invalid = kCMTimeInvalid
init(videoFramesPerSecond: Int) {
self.init(seconds: 1 / Double(videoFramesPerSecond), preferredTimescale: .video)
}
} }
extension CGDirectDisplayID { extension CGDirectDisplayID {
@ -77,3 +305,4 @@ extension NSScreen {
return name return name
} }
} }
// MARK: -