143 lines
4.6 KiB
Swift
143 lines
4.6 KiB
Swift
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+)
|
||
// CGRequestScreenCaptureAccess 在首次调用时触发权限弹窗
|
||
#if canImport(ScreenCaptureKit)
|
||
if !CGPreflightScreenCaptureAccess() {
|
||
CGRequestScreenCaptureAccess()
|
||
}
|
||
#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
|