MiniPlayer/Sources/ScreenRecorder.swift

141 lines
4.5 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import AVFoundation
#if os(macOS)
import AppKit
/// + ~/Movies/MiniPlayer/
@MainActor
final class ScreenRecorder: NSObject, AVCaptureFileOutputRecordingDelegate {
private var session: AVCaptureSession?
private var fileOutput: AVCaptureMovieFileOutput?
private var isConfigured = false
@Published var isRecording = false
var onRecordingStopped: ((URL) -> Void)?
var onError: ((String) -> Void)?
///
private var saveDirectory: URL {
let movies = FileManager.default.urls(for: .moviesDirectory, in: .userDomainMask).first!
let dir = movies.appendingPathComponent("MiniPlayer", isDirectory: true)
try? FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true)
return dir
}
///
func startRecording() {
guard !isRecording else { return }
// session
if !isConfigured {
guard configureSession() else { return }
isConfigured = true
}
guard let fileOutput = fileOutput else {
onError?("Recording not ready")
return
}
// _2026-06-24_15-30-45.mp4
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd_HH-mm-ss"
let filename = "Recording_\(formatter.string(from: Date())).mp4"
let outputURL = saveDirectory.appendingPathComponent(filename)
// session
if let session = session, !session.isRunning {
session.startRunning()
}
fileOutput.startRecording(to: outputURL, recordingDelegate: self)
}
///
func stopRecording() {
guard isRecording, let fileOutput = fileOutput else { return }
fileOutput.stopRecording()
}
private func configureSession() -> Bool {
let session = AVCaptureSession()
session.sessionPreset = .hd1920x1080
//
guard let displayID = NSScreen.main?.deviceDescription[NSDeviceDescriptionKey("NSScreenNumber")] as? CGDirectDisplayID else {
onError?("No display found")
return false
}
let screenInput = AVCaptureScreenInput(displayID: displayID)
guard let screenInput = screenInput else {
onError?("Cannot create screen input")
return false
}
screenInput.capturesMouseClicks = false
screenInput.capturesCursor = true
if session.canAddInput(screenInput) {
session.addInput(screenInput)
} else {
onError?("Cannot add screen input")
return false
}
//
if let audioDevice = AVCaptureDevice.default(for: .audio),
let audioInput = try? AVCaptureDeviceInput(device: audioDevice) {
if session.canAddInput(audioInput) {
session.addInput(audioInput)
}
}
//
//
let output = AVCaptureMovieFileOutput()
if session.canAddOutput(output) {
session.addOutput(output)
} else {
onError?("Cannot add file output")
return false
}
self.session = session
self.fileOutput = output
// macOS 10.15+
// CGPreflightScreenCaptureAccess
#if canImport(ScreenCaptureKit)
CGPreflightScreenCaptureAccess()
#endif
return true
}
// MARK: - AVCaptureFileOutputRecordingDelegate
nonisolated func fileOutputRecordingDidStart(_ output: AVCaptureFileOutput) {
Task { @MainActor in
self.isRecording = true
}
}
nonisolated func fileOutput(_ output: AVCaptureFileOutput, didFinishRecordingTo outputFileURL: URL, from connections: [AVCaptureConnection], error: Error?) {
Task { @MainActor in
self.isRecording = false
if let error = error {
self.onError?("Recording failed: \(error.localizedDescription)")
} else {
self.onRecordingStopped?(outputFileURL)
}
// session
self.session?.stopRunning()
}
}
deinit {
session?.stopRunning()
}
}
#endif