aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRuben Beltran del Rio <ruben@unlimited.pizza>2023-07-28 08:53:26 +0200
committerRuben Beltran del Rio <ruben@unlimited.pizza>2023-07-28 08:53:26 +0200
commitf5d16c1c8c259966338b23afd407d142f72784aa (patch)
treed8ef63436af5c954dac262b4766ece363ac10ac9
parenta4e804275517af683afa1733e2db7c383c306f2b (diff)
Separate some logic out of main app
-rw-r--r--Captura.xcodeproj/project.pbxproj22
-rw-r--r--Captura/Core Extensions/CGImage+resize.swift21
-rw-r--r--Captura/Core Extensions/Notification+AppEvents.swift (renamed from Captura/Data/Notification+AppEvents.swift)0
-rw-r--r--Captura/Data/CapturaFile.swift31
-rw-r--r--Captura/Data/GifRenderer.swift19
-rw-r--r--Captura/Presentation/Windows/CapturaApp.swift117
6 files changed, 128 insertions, 82 deletions
diff --git a/Captura.xcodeproj/project.pbxproj b/Captura.xcodeproj/project.pbxproj
index 98480e1..a506dc5 100644
--- a/Captura.xcodeproj/project.pbxproj
+++ b/Captura.xcodeproj/project.pbxproj
@@ -12,6 +12,9 @@
B5278B212A71BFC3009F6462 /* SettingsStructs.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5278B202A71BFC3009F6462 /* SettingsStructs.swift */; };
B5278B232A71C140009F6462 /* PreferencesWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5278B222A71C140009F6462 /* PreferencesWindow.swift */; };
B5278B252A71CA80009F6462 /* AdvancedSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5278B242A71CA80009F6462 /* AdvancedSettings.swift */; };
+ B5278B282A739871009F6462 /* CGImage+resize.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5278B272A739871009F6462 /* CGImage+resize.swift */; };
+ B5278B2A2A73992D009F6462 /* GifRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5278B292A73992D009F6462 /* GifRenderer.swift */; };
+ B5278B2C2A739B3A009F6462 /* CapturaFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5278B2B2A739B3A009F6462 /* CapturaFile.swift */; };
B55DDFCC2A6F0253001A5E76 /* Notification+AppEvents.swift in Sources */ = {isa = PBXBuildFile; fileRef = B55DDFCB2A6F0253001A5E76 /* Notification+AppEvents.swift */; };
B55DDFCE2A6F069D001A5E76 /* RecordingWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = B55DDFCD2A6F069D001A5E76 /* RecordingWindow.swift */; };
B56C70CD2A6EFDF4009B97EB /* CaptureState.swift in Sources */ = {isa = PBXBuildFile; fileRef = B56C70CC2A6EFDF4009B97EB /* CaptureState.swift */; };
@@ -48,6 +51,9 @@
B5278B202A71BFC3009F6462 /* SettingsStructs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsStructs.swift; sourceTree = "<group>"; };
B5278B222A71C140009F6462 /* PreferencesWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferencesWindow.swift; sourceTree = "<group>"; };
B5278B242A71CA80009F6462 /* AdvancedSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdvancedSettings.swift; sourceTree = "<group>"; };
+ B5278B272A739871009F6462 /* CGImage+resize.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CGImage+resize.swift"; sourceTree = "<group>"; };
+ B5278B292A73992D009F6462 /* GifRenderer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GifRenderer.swift; sourceTree = "<group>"; };
+ B5278B2B2A739B3A009F6462 /* CapturaFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CapturaFile.swift; sourceTree = "<group>"; };
B55DDFCB2A6F0253001A5E76 /* Notification+AppEvents.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Notification+AppEvents.swift"; sourceTree = "<group>"; };
B55DDFCD2A6F069D001A5E76 /* RecordingWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecordingWindow.swift; sourceTree = "<group>"; };
B56C70CC2A6EFDF4009B97EB /* CaptureState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CaptureState.swift; sourceTree = "<group>"; };
@@ -131,8 +137,9 @@
isa = PBXGroup;
children = (
B56C70CC2A6EFDF4009B97EB /* CaptureState.swift */,
- B55DDFCB2A6F0253001A5E76 /* Notification+AppEvents.swift */,
B5278B202A71BFC3009F6462 /* SettingsStructs.swift */,
+ B5278B292A73992D009F6462 /* GifRenderer.swift */,
+ B5278B2B2A739B3A009F6462 /* CapturaFile.swift */,
);
path = Data;
sourceTree = "<group>";
@@ -146,6 +153,15 @@
path = Settings;
sourceTree = "<group>";
};
+ B5278B262A739862009F6462 /* Core Extensions */ = {
+ isa = PBXGroup;
+ children = (
+ B55DDFCB2A6F0253001A5E76 /* Notification+AppEvents.swift */,
+ B5278B272A739871009F6462 /* CGImage+resize.swift */,
+ );
+ path = "Core Extensions";
+ sourceTree = "<group>";
+ };
B5F915452A6EF80D007ECE8E = {
isa = PBXGroup;
children = (
@@ -169,6 +185,7 @@
B5F915502A6EF80D007ECE8E /* Captura */ = {
isa = PBXGroup;
children = (
+ B5278B262A739862009F6462 /* Core Extensions */,
B5278B1C2A71BD3C009F6462 /* Data */,
B5278B182A71BD10009F6462 /* Presentation */,
B5F915552A6EF80E007ECE8E /* Assets.xcassets */,
@@ -335,7 +352,10 @@
buildActionMask = 2147483647;
files = (
B5F915542A6EF80D007ECE8E /* PreferencesScreen.swift in Sources */,
+ B5278B282A739871009F6462 /* CGImage+resize.swift in Sources */,
+ B5278B2C2A739B3A009F6462 /* CapturaFile.swift in Sources */,
B5278B1F2A71BD9B009F6462 /* OutputSettings.swift in Sources */,
+ B5278B2A2A73992D009F6462 /* GifRenderer.swift in Sources */,
B5278B212A71BFC3009F6462 /* SettingsStructs.swift in Sources */,
B55DDFCE2A6F069D001A5E76 /* RecordingWindow.swift in Sources */,
B55DDFCC2A6F0253001A5E76 /* Notification+AppEvents.swift in Sources */,
diff --git a/Captura/Core Extensions/CGImage+resize.swift b/Captura/Core Extensions/CGImage+resize.swift
new file mode 100644
index 0000000..110e85b
--- /dev/null
+++ b/Captura/Core Extensions/CGImage+resize.swift
@@ -0,0 +1,21 @@
+import CoreGraphics
+
+extension CGImage {
+ func resize(by scale: CGFloat) -> CGImage? {
+ let width = Int(CGFloat(self.width) / scale)
+ let height = Int(CGFloat(self.height) / scale)
+
+ let bitsPerComponent = self.bitsPerComponent
+ let colorSpace = self.colorSpace ?? CGColorSpace(name: CGColorSpace.sRGB)!
+ let bitmapInfo = self.bitmapInfo.rawValue
+
+ guard let context = CGContext(data: nil, width: width, height: height, bitsPerComponent: bitsPerComponent, bytesPerRow: 0, space: colorSpace, bitmapInfo: bitmapInfo) else {
+ return nil
+ }
+
+ context.interpolationQuality = .high
+ context.draw(self, in: CGRect(x: 0, y: 0, width: width, height: height))
+
+ return context.makeImage()
+ }
+}
diff --git a/Captura/Data/Notification+AppEvents.swift b/Captura/Core Extensions/Notification+AppEvents.swift
index 809c00e..809c00e 100644
--- a/Captura/Data/Notification+AppEvents.swift
+++ b/Captura/Core Extensions/Notification+AppEvents.swift
diff --git a/Captura/Data/CapturaFile.swift b/Captura/Data/CapturaFile.swift
new file mode 100644
index 0000000..da1f2fb
--- /dev/null
+++ b/Captura/Data/CapturaFile.swift
@@ -0,0 +1,31 @@
+import Foundation
+
+struct CapturaFile {
+
+ let name: String
+ let baseDirectory: URL
+ let appDirectory: String = "captura"
+
+ private var baseURL: URL {
+ baseDirectory.appendingPathComponent("\(appDirectory)/\(name)")
+ }
+
+ var mp4URL: URL {
+ return baseURL.appendingPathExtension("mp4")
+ }
+
+ var gifURL: URL {
+ return baseURL.appendingPathExtension("gif")
+ }
+
+ init() {
+ let dateFormatter = DateFormatter()
+ dateFormatter.dateStyle = .medium
+ dateFormatter.timeStyle = .medium
+ dateFormatter.locale = Locale.current
+ let dateString = dateFormatter.string(from: Date()).replacingOccurrences(of: ":", with: ".")
+
+ self.name = "Captura \(dateString)"
+ self.baseDirectory = FileManager.default.urls(for: .picturesDirectory, in: .userDomainMask).first!
+ }
+}
diff --git a/Captura/Data/GifRenderer.swift b/Captura/Data/GifRenderer.swift
new file mode 100644
index 0000000..208d7db
--- /dev/null
+++ b/Captura/Data/GifRenderer.swift
@@ -0,0 +1,19 @@
+import UniformTypeIdentifiers
+import SwiftUI
+import CoreGraphics
+
+struct GifRenderer {
+ static func render(_ images: [CGImage], at fps: Int, to url: URL) async {
+ let framedelay = String(format: "%.3f", 1.0 / Double(fps))
+ let fileProperties = [kCGImagePropertyGIFDictionary as String: [kCGImagePropertyGIFLoopCount as String: 0]]
+ let gifProperties = [kCGImagePropertyGIFDictionary as String: [kCGImagePropertyGIFUnclampedDelayTime as String: framedelay]]
+ let cfURL = url as CFURL
+ if let destination = CGImageDestinationCreateWithURL(cfURL, UTType.gif.identifier as CFString, images.count, nil) {
+ CGImageDestinationSetProperties(destination, fileProperties as CFDictionary?)
+ for image in images {
+ CGImageDestinationAddImage(destination, image, gifProperties as CFDictionary?)
+ }
+ CGImageDestinationFinalize(destination)
+ }
+ }
+}
diff --git a/Captura/Presentation/Windows/CapturaApp.swift b/Captura/Presentation/Windows/CapturaApp.swift
index 87e2560..6cf7044 100644
--- a/Captura/Presentation/Windows/CapturaApp.swift
+++ b/Captura/Presentation/Windows/CapturaApp.swift
@@ -33,7 +33,7 @@ class CapturaAppDelegate: NSObject, NSApplicationDelegate, AVCaptureVideoDataOut
var receivedFrames = false
var captureSession: AVCaptureSession? = nil
var images: [CGImage] = []
- var outputURL: URL? = nil
+ var outputFile: CapturaFile? = nil
var gifCallbackTimer = ContinuousClock.now
var fps = UserDefaults.standard.integer(forKey: "frameRate")
var pixelDensity: CGFloat = 1.0
@@ -93,7 +93,7 @@ class CapturaAppDelegate: NSObject, NSApplicationDelegate, AVCaptureVideoDataOut
if captureState != .idle {
menu.cancelTracking()
if captureState == .recording {
- stopRecording()
+ NotificationCenter.default.post(name: .stopRecording, object: nil, userInfo: nil)
}
}
}
@@ -115,14 +115,6 @@ class CapturaAppDelegate: NSObject, NSApplicationDelegate, AVCaptureVideoDataOut
NSApplication.shared.terminate(self)
}
- @objc private func onClickStatusBar(_ sender: NSStatusBarButton) {
- print("CLICK")
- if captureState == .recording {
- stopRecording()
- }
- }
-
-
// MARK: - App State Event Listeners
@objc func didReceiveNotification(_ notification: Notification) {
@@ -134,7 +126,9 @@ class CapturaAppDelegate: NSObject, NSApplicationDelegate, AVCaptureVideoDataOut
case .stopRecording:
stopRecording()
case .finalizeRecording:
- finalizeRecording()
+ DispatchQueue.main.async {
+ self.finalizeRecording()
+ }
case .reset:
reset()
default:
@@ -177,7 +171,7 @@ class CapturaAppDelegate: NSObject, NSApplicationDelegate, AVCaptureVideoDataOut
func startRecording() {
captureState = .recording
fps = UserDefaults.standard.integer(forKey: "frameRate")
- outputURL = nil
+ outputFile = nil
images = [];
pixelDensity = recordingWindow?.pixelDensity ?? 1.0
if let view = recordingWindow?.contentView as? RecordingContentView {
@@ -215,19 +209,16 @@ class CapturaAppDelegate: NSObject, NSApplicationDelegate, AVCaptureVideoDataOut
DispatchQueue.main.asyncAfter(deadline: .now() + 300, execute: stopTimer!)
if let button = statusItem.button {
- button.image = NSImage(systemSymbolName: "stop.circle", accessibilityDescription: "Captura")
+ button.image = NSImage(systemSymbolName: "checkmark.rectangle", accessibilityDescription: "Captura")
}
receivedFrames = false
captureSession.startRunning()
- guard let picturesDirectoryURL = FileManager.default.urls(for: .picturesDirectory, in: .userDomainMask).first else {
- fatalError("Unable to access user's Pictures directory")
- }
- outputURL = picturesDirectoryURL.appendingPathComponent("captura/\(filename())").appendingPathExtension("mp4")
+ outputFile = CapturaFile()
let outputFormatsSetting = OutputFormatSetting(rawValue: UserDefaults.standard.integer(forKey: "outputFormats")) ?? .all
if outputFormatsSetting.shouldSaveMp4() {
- movieFileOutput.startRecording(to: outputURL!, recordingDelegate: self)
+ movieFileOutput.startRecording(to: outputFile!.mp4URL, recordingDelegate: self)
}
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
@@ -246,21 +237,39 @@ class CapturaAppDelegate: NSObject, NSApplicationDelegate, AVCaptureVideoDataOut
}
func stopRecording() {
+ if let button = statusItem.button {
+ button.image = NSImage(systemSymbolName: "dock.arrow.up.rectangle", accessibilityDescription: "Captura")
+ }
stopTimer?.cancel()
captureState = .uploading
captureSession?.stopRunning()
captureSession = nil
+ boxListener?.cancel()
+ recordingWindow?.close()
+ self.recordingWindow = nil
+
+ let outputFormatsSetting = OutputFormatSetting(rawValue: UserDefaults.standard.integer(forKey: "outputFormats")) ?? .all
+ if !outputFormatsSetting.shouldSaveGif() {
+ NotificationCenter.default.post(name: .finalizeRecording, object: nil, userInfo: nil)
+ return
+ }
+
Task.detached {
- if let outputURL = self.outputURL {
- await self.createGif(url: outputURL.deletingPathExtension().appendingPathExtension("gif"))
+ if let outputFile = self.outputFile {
+ await GifRenderer.render(self.images, at: self.fps, to: outputFile.gifURL)
+ NotificationCenter.default.post(name: .finalizeRecording, object: nil, userInfo: nil)
}
}
- reset()
}
func finalizeRecording() {
+ if let button = statusItem.button {
+ button.image = NSImage(systemSymbolName: "checkmark.rectangle.fill", accessibilityDescription: "Captura")
+ }
captureState = .uploaded
- // Stopping the recording
+ DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) {
+ self.reset()
+ }
}
func reset() {
@@ -268,6 +277,8 @@ class CapturaAppDelegate: NSObject, NSApplicationDelegate, AVCaptureVideoDataOut
button.image = NSImage(systemSymbolName: "rectangle.dashed.badge.record", accessibilityDescription: "Captura")
}
captureState = .idle
+ stopTimer?.cancel()
+ captureSession?.stopRunning()
boxListener?.cancel()
recordingWindow?.close()
self.recordingWindow = nil
@@ -294,7 +305,7 @@ class CapturaAppDelegate: NSObject, NSApplicationDelegate, AVCaptureVideoDataOut
let ciImage = CIImage(cvImageBuffer: imageBuffer)
let context = CIContext()
if let cgImage = context.createCGImage(ciImage, from: CGRect(x: 0, y: 0, width: CVPixelBufferGetWidth(imageBuffer), height: CVPixelBufferGetHeight(imageBuffer))) {
- if let cgImage = self.resize(image: cgImage, by: self.pixelDensity) {
+ if let cgImage = cgImage.resize(by: self.pixelDensity) {
self.images.append(cgImage)
}
}
@@ -302,16 +313,6 @@ class CapturaAppDelegate: NSObject, NSApplicationDelegate, AVCaptureVideoDataOut
}
}
- func fileOutput(_ output: AVCaptureFileOutput, didFinishRecordingTo outputFileURL: URL, from connections: [AVCaptureConnection], error: Error?) {
- if let error = error as? NSError {
- if error.domain == AVFoundationErrorDomain && error.code == -11806 {
- Task.detached {
- await self.createGif(url: outputFileURL.deletingPathExtension().appendingPathExtension("gif"))
- }
- }
- }
- }
-
private func showPopoverWithMessage(_ message: String) {
if let button = statusItem.button {
(self.popover?.contentViewController as? HelpPopoverViewController)?.updateLabel(message)
@@ -322,53 +323,7 @@ class CapturaAppDelegate: NSObject, NSApplicationDelegate, AVCaptureVideoDataOut
}
}
- func filename() -> String {
- let dateFormatter = DateFormatter()
- dateFormatter.dateStyle = .medium
- dateFormatter.timeStyle = .medium
- dateFormatter.locale = Locale.current
- let dateString = dateFormatter.string(from: Date()).replacingOccurrences(of: ":", with: ".")
-
- return "Captura \(dateString)"
- }
-
- func createGif(url: URL) async {
-
-
- let outputFormatsSetting = OutputFormatSetting(rawValue: UserDefaults.standard.integer(forKey: "outputFormats")) ?? .all
- if !outputFormatsSetting.shouldSaveGif() {
- return
- }
-
- let framedelay = String(format: "%.3f", 1.0 / Double(fps))
- let fileProperties = [kCGImagePropertyGIFDictionary as String: [kCGImagePropertyGIFLoopCount as String: 0]]
- let gifProperties = [kCGImagePropertyGIFDictionary as String: [kCGImagePropertyGIFUnclampedDelayTime as String: framedelay]]
- let cfURL = url as CFURL
- if let destination = CGImageDestinationCreateWithURL(cfURL, UTType.gif.identifier as CFString, images.count, nil) {
- CGImageDestinationSetProperties(destination, fileProperties as CFDictionary?)
- for image in images {
- CGImageDestinationAddImage(destination, image, gifProperties as CFDictionary?)
- }
- CGImageDestinationFinalize(destination)
- }
- }
+ // MARK: - AVCaptureFileOutputRecordingDelegate Implementation
- private func resize(image: CGImage, by scale: CGFloat) -> CGImage? {
- let width = Int(CGFloat(image.width) / scale)
- let height = Int(CGFloat(image.height) / scale)
-
- let bitsPerComponent = image.bitsPerComponent
- let colorSpace = image.colorSpace ?? CGColorSpace(name: CGColorSpace.sRGB)!
- let bitmapInfo = image.bitmapInfo.rawValue
-
- guard let context = CGContext(data: nil, width: width, height: height, bitsPerComponent: bitsPerComponent, bytesPerRow: 0, space: colorSpace, bitmapInfo: bitmapInfo) else {
- return nil
- }
-
- context.interpolationQuality = .high
- context.draw(image, in: CGRect(x: 0, y: 0, width: width, height: height))
-
- return context.makeImage()
- }
-
+ func fileOutput(_ output: AVCaptureFileOutput, didFinishRecordingTo outputFileURL: URL, from connections: [AVCaptureConnection], error: Error?) {}
}