X-Git-Url: https://git.r.bdr.sh/rbdr/captura/blobdiff_plain/a4e804275517af683afa1733e2db7c383c306f2b..f5d16c1c8c259966338b23afd407d142f72784aa:/Captura/Presentation/Windows/CapturaApp.swift?ds=inline 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?) {} }