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?) {}
}