Various Swift code improvements (#58)
This commit is contained in:
parent
e9184b38a3
commit
e5d4ddf60f
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
try record()
|
||||
exit(0)
|
||||
}
|
||||
|
||||
usage()
|
||||
case .none:
|
||||
showUsage()
|
||||
exit(1)
|
||||
default:
|
||||
try record()
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
private var stderr = StandardErrorOutputStream()
|
||||
typealias CSignalHandler = @convention(c) (Int32) -> Void
|
||||
typealias SignalHandler = (Signal) -> Void
|
||||
|
||||
func printErr(_ item: Any) {
|
||||
print(item, to: &stderr)
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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: -
|
||||
|
|
Loading…
Reference in New Issue