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`
init(
destination: URL,
fps: Int,
framesPerSecond: Int,
cropRect: CGRect?,
showCursor: Bool,
highlightClicks: Bool,
@ -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

View File

@ -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
@ -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,
@ -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:
@ -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)

View File

@ -1,16 +1,236 @@
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)
}
}
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 {
@ -18,9 +238,17 @@ func toJson<T>(_ data: T) throws -> String {
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 {
@ -77,3 +305,4 @@ extension NSScreen {
return name
}
}
// MARK: -