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 */; };
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>"; };
isa = PBXGroup;
children = (
B56C70CC2A6EFDF4009B97EB /* CaptureState.swift */,
- B55DDFCB2A6F0253001A5E76 /* Notification+AppEvents.swift */,
B5278B202A71BFC3009F6462 /* SettingsStructs.swift */,
+ B5278B292A73992D009F6462 /* GifRenderer.swift */,
+ B5278B2B2A739B3A009F6462 /* CapturaFile.swift */,
);
path = Data;
sourceTree = "<group>";
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 = (
B5F915502A6EF80D007ECE8E /* Captura */ = {
isa = PBXGroup;
children = (
+ B5278B262A739862009F6462 /* Core Extensions */,
B5278B1C2A71BD3C009F6462 /* Data */,
B5278B182A71BD10009F6462 /* Presentation */,
B5F915552A6EF80E007ECE8E /* Assets.xcassets */,
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 */,
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
if captureState != .idle {
menu.cancelTracking()
if captureState == .recording {
- stopRecording()
+ NotificationCenter.default.post(name: .stopRecording, object: nil, userInfo: nil)
}
}
}
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) {
case .stopRecording:
stopRecording()
case .finalizeRecording:
- finalizeRecording()
+ DispatchQueue.main.async {
+ self.finalizeRecording()
+ }
case .reset:
reset()
default:
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 {
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) {
}
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() {
button.image = NSImage(systemSymbolName: "rectangle.dashed.badge.record", accessibilityDescription: "Captura")
}
captureState = .idle
+ stopTimer?.cancel()
+ captureSession?.stopRunning()
boxListener?.cancel()
recordingWindow?.close()
self.recordingWindow = nil
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)
}
}
}
}
- 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)
}
}
- 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?) {}
}