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`
|
/// 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
|
||||||
|
|
|
@ -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)
|
|
||||||
|
|
|
@ -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: -
|
||||||
|
|
Loading…
Reference in New Issue