]> git.r.bdr.sh - rbdr/captura/blobdiff - Captura/Presentation/Windows/CapturaApp.swift
Separate some logic out of main app
[rbdr/captura] / Captura / Presentation / Windows / CapturaApp.swift
index 87e25604ce6044ddc4048d22c9ac9ac95e8a5822..6cf70445038f54856ac745ab48b2f46830586477 100644 (file)
@@ -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?) {}
 }